diff --git a/src/components/layout/Stepper.tsx b/src/components/layout/Stepper.tsx new file mode 100644 index 00000000..d0c9c499 --- /dev/null +++ b/src/components/layout/Stepper.tsx @@ -0,0 +1,25 @@ +export interface StepperProps { + current: number; + steps: number; + className?: string; +} + +export function Stepper(props: StepperProps) { + const percentage = (props.current / (props.steps + 1)) * 100; + + return ( +
+

+ {props.current}/{props.steps} +

+
+
+
+
+ ); +} diff --git a/src/components/layout/ThinContainer.tsx b/src/components/layout/ThinContainer.tsx index f7f90acb..345afa5c 100644 --- a/src/components/layout/ThinContainer.tsx +++ b/src/components/layout/ThinContainer.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { ReactNode } from "react"; interface ThinContainerProps { @@ -16,3 +17,16 @@ export function ThinContainer(props: ThinContainerProps) {
); } + +export function CenterContainer(props: ThinContainerProps) { + return ( +
+
{props.children}
+
+ ); +} diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index aa73bd83..5282d9b3 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -1,6 +1,12 @@ import { RunOutput } from "@movie-web/providers"; import { useCallback, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { + Navigate, + useLocation, + useNavigate, + useParams, +} from "react-router-dom"; +import { useAsync } from "react-use"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; @@ -15,9 +21,10 @@ import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; import { useLastNonPlayerLink } from "@/stores/history"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; +import { needsOnboarding } from "@/utils/onboarding"; import { parseTimestamp } from "@/utils/timestamp"; -export function PlayerView() { +export function RealPlayerView() { const navigate = useNavigate(); const params = useParams<{ media: string; @@ -109,4 +116,25 @@ export function PlayerView() { ); } +export function PlayerView() { + const loc = useLocation(); + const { loading, error, value } = useAsync(() => { + return needsOnboarding(); + }); + + if (error) throw new Error("Failed to detect onboarding"); + if (loading) return null; + if (value) + return ( + + ); + return ; +} + export default PlayerView; diff --git a/src/pages/layouts/MinimalPageLayout.tsx b/src/pages/layouts/MinimalPageLayout.tsx new file mode 100644 index 00000000..6d2cf34d --- /dev/null +++ b/src/pages/layouts/MinimalPageLayout.tsx @@ -0,0 +1,28 @@ +import { Link } from "react-router-dom"; + +import { BrandPill } from "@/components/layout/BrandPill"; +import { BlurEllipsis } from "@/pages/layouts/SubPageLayout"; + +export function MinimalPageLayout(props: { children: React.ReactNode }) { + return ( +
+ + {/* Main page */} +
+ + + +
+
{props.children}
+
+ ); +} diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx new file mode 100644 index 00000000..439cc87d --- /dev/null +++ b/src/pages/onboarding/Onboarding.tsx @@ -0,0 +1,48 @@ +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/buttons/Button"; +import { Stepper } from "@/components/layout/Stepper"; +import { CenterContainer } from "@/components/layout/ThinContainer"; +import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; + +export function OnboardingPage() { + const navigate = useNavigate(); + const skipModal = useModal("skip"); + const { skipAndRedirect } = useRedirectBack(); + + return ( + + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum + + + + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + + + + + ); +} diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx new file mode 100644 index 00000000..8ee85be7 --- /dev/null +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -0,0 +1,27 @@ +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/buttons/Button"; +import { Stepper } from "@/components/layout/Stepper"; +import { CenterContainer } from "@/components/layout/ThinContainer"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; + +export function OnboardingExtensionPage() { + const navigate = useNavigate(); + + return ( + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + + + + ); +} diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx new file mode 100644 index 00000000..45d22aea --- /dev/null +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -0,0 +1,27 @@ +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/buttons/Button"; +import { Stepper } from "@/components/layout/Stepper"; +import { CenterContainer } from "@/components/layout/ThinContainer"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; + +export function OnboardingProxyPage() { + const navigate = useNavigate(); + + return ( + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + + + + ); +} diff --git a/src/pages/onboarding/onboardingHooks.ts b/src/pages/onboarding/onboardingHooks.ts new file mode 100644 index 00000000..e80234e7 --- /dev/null +++ b/src/pages/onboarding/onboardingHooks.ts @@ -0,0 +1,22 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useQueryParam } from "@/hooks/useQueryParams"; +import { useOnboardingStore } from "@/stores/onboarding"; + +export function useRedirectBack() { + const [url] = useQueryParam("redirect"); + const navigate = useNavigate(); + const setSkipped = useOnboardingStore((s) => s.setSkipped); + + const redirectBack = useCallback(() => { + navigate(url ?? "/"); + }, [navigate, url]); + + const skipAndRedirect = useCallback(() => { + setSkipped(true); + redirectBack(); + }, [redirectBack, setSkipped]); + + return { redirectBack, skipAndRedirect }; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index afa5f8ba..37c55e0f 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -19,6 +19,9 @@ import { DmcaPage, shouldHaveDmcaPage } from "@/pages/Dmca"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; import { HomePage } from "@/pages/HomePage"; import { LoginPage } from "@/pages/Login"; +import { OnboardingPage } from "@/pages/onboarding/Onboarding"; +import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension"; +import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy"; import { RegisterPage } from "@/pages/Register"; import { Layout } from "@/setup/Layout"; import { useHistoryListener } from "@/stores/history"; @@ -119,6 +122,12 @@ function App() { } /> } /> } /> + } /> + } + /> + } /> {shouldHaveDmcaPage() ? ( } /> diff --git a/src/setup/config.ts b/src/setup/config.ts index 2e1634a4..d6aca1cb 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -19,6 +19,7 @@ interface Config { DISALLOWED_IDS: string; TURNSTILE_KEY: string; CDN_REPLACEMENTS: string; + HAS_ONBOARDING: string; } export interface RuntimeConfig { @@ -34,6 +35,7 @@ export interface RuntimeConfig { DISALLOWED_IDS: string[]; TURNSTILE_KEY: string | null; CDN_REPLACEMENTS: Array; + HAS_ONBOARDING: boolean; } const env: Record = { @@ -49,6 +51,7 @@ const env: Record = { DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS, TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY, CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS, + HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING, }; // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js) @@ -82,6 +85,7 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", + HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true", TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null, DISALLOWED_IDS: getKey("DISALLOWED_IDS", "") .split(",") diff --git a/src/stores/history/index.ts b/src/stores/history/index.ts index c90a7309..c1f507ee 100644 --- a/src/stores/history/index.ts +++ b/src/stores/history/index.ts @@ -46,7 +46,8 @@ export function useLastNonPlayerLink() { (v) => !v.path.startsWith("/media") && // cannot be a player link location.pathname !== v.path && // cannot be current link - !v.path.startsWith("/s/"), // cannot be a quick search link + !v.path.startsWith("/s/") && // cannot be a quick search link + !v.path.startsWith("/onboarding"), // cannot be an onboarding link ); return route?.path ?? "/"; }, [routes, location]); diff --git a/src/stores/onboarding/index.tsx b/src/stores/onboarding/index.tsx new file mode 100644 index 00000000..17d1965d --- /dev/null +++ b/src/stores/onboarding/index.tsx @@ -0,0 +1,22 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +export interface OnboardingStore { + skipped: boolean; + setSkipped(v: boolean): void; +} + +export const useOnboardingStore = create( + persist( + immer((set) => ({ + skipped: false, + setSkipped(v) { + set((s) => { + s.skipped = v; + }); + }, + })), + { name: "__MW::onboarding" }, + ), +); diff --git a/src/utils/onboarding.ts b/src/utils/onboarding.ts new file mode 100644 index 00000000..7289fd1c --- /dev/null +++ b/src/utils/onboarding.ts @@ -0,0 +1,23 @@ +import { isExtensionActive } from "@/backend/extension/messaging"; +import { conf } from "@/setup/config"; +import { useAuthStore } from "@/stores/auth"; +import { useOnboardingStore } from "@/stores/onboarding"; + +export async function needsOnboarding(): Promise { + // if onboarding is dislabed, no onboarding needed + if (!conf().HAS_ONBOARDING) return false; + + // if extension is active and working, no onboarding needed + const extensionActive = await isExtensionActive(); + if (extensionActive) return false; + + // if there is any custom proxy urls, no onboarding needed + const proxyUrls = useAuthStore.getState().proxySet; + if (proxyUrls) return false; + + // if onboarding has been skipped, no onboarding needed + const skipped = useOnboardingStore.getState().skipped; + if (skipped) return false; + + return true; +}