mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
Basic onboarding structure
This commit is contained in:
parent
fa2b610ea6
commit
925f3dff19
13 changed files with 281 additions and 3 deletions
25
src/components/layout/Stepper.tsx
Normal file
25
src/components/layout/Stepper.tsx
Normal file
|
@ -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 (
|
||||
<div className={props.className}>
|
||||
<p className="mb-2">
|
||||
{props.current}/{props.steps}
|
||||
</p>
|
||||
<div className="max-w-full h-1 w-32 bg-white rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-[width] rounded-full"
|
||||
style={{
|
||||
width: `${percentage.toFixed(0)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ThinContainerProps {
|
||||
|
@ -16,3 +17,16 @@ export function ThinContainer(props: ThinContainerProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CenterContainer(props: ThinContainerProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"min-h-screen w-full flex justify-center p-8 items-center",
|
||||
props.classNames,
|
||||
)}
|
||||
>
|
||||
<div className="w-[600px] max-w-full">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/onboarding",
|
||||
search: `redirect=${encodeURIComponent(loc.pathname)}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <RealPlayerView />;
|
||||
}
|
||||
|
||||
export default PlayerView;
|
||||
|
|
28
src/pages/layouts/MinimalPageLayout.tsx
Normal file
28
src/pages/layouts/MinimalPageLayout.tsx
Normal file
|
@ -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 (
|
||||
<div
|
||||
className="bg-background-main min-h-screen"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)",
|
||||
}}
|
||||
>
|
||||
<BlurEllipsis />
|
||||
{/* Main page */}
|
||||
<div className="fixed px-7 py-5 left-0 top-0">
|
||||
<Link
|
||||
className="block tabbable rounded-full text-xs ssm:text-base"
|
||||
to="/"
|
||||
>
|
||||
<BrandPill clickable />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="min-h-screen">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
48
src/pages/onboarding/Onboarding.tsx
Normal file
48
src/pages/onboarding/Onboarding.tsx
Normal file
|
@ -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 (
|
||||
<MinimalPageLayout>
|
||||
<PageTitle subpage k="global.pages.about" />
|
||||
<Modal id={skipModal.id}>
|
||||
<ModalCard>
|
||||
<ModalCard>
|
||||
<Heading2 className="!mt-0">Lorem ipsum</Heading2>
|
||||
<Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph>
|
||||
<Button theme="secondary" onClick={skipModal.hide}>
|
||||
Lorem ipsum
|
||||
</Button>
|
||||
<Button theme="danger" onClick={() => skipAndRedirect()}>
|
||||
Lorem ipsum
|
||||
</Button>
|
||||
</ModalCard>
|
||||
</ModalCard>
|
||||
</Modal>
|
||||
<CenterContainer>
|
||||
<Stepper steps={2} current={1} className="mb-12" />
|
||||
<Heading2 className="!mt-0">Lorem ipsum</Heading2>
|
||||
<Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph>
|
||||
<Button onClick={() => navigate("/onboarding/proxy")}>
|
||||
Custom proxy
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/onboarding/extension")}>
|
||||
Extension
|
||||
</Button>
|
||||
<Button onClick={skipModal.show}>Default</Button>
|
||||
</CenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
}
|
27
src/pages/onboarding/OnboardingExtension.tsx
Normal file
27
src/pages/onboarding/OnboardingExtension.tsx
Normal file
|
@ -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 (
|
||||
<MinimalPageLayout>
|
||||
<PageTitle subpage k="global.pages.about" />
|
||||
<CenterContainer>
|
||||
<Stepper steps={2} current={2} className="mb-12" />
|
||||
<Heading2 className="!mt-0">Lorem ipsum</Heading2>
|
||||
<Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph>
|
||||
<Button onClick={() => navigate("/onboarding")}>Back</Button>
|
||||
<Button onClick={() => alert("Check extension here or something")}>
|
||||
Check extension
|
||||
</Button>
|
||||
</CenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
}
|
27
src/pages/onboarding/OnboardingProxy.tsx
Normal file
27
src/pages/onboarding/OnboardingProxy.tsx
Normal file
|
@ -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 (
|
||||
<MinimalPageLayout>
|
||||
<PageTitle subpage k="global.pages.about" />
|
||||
<CenterContainer>
|
||||
<Stepper steps={2} current={2} className="mb-12" />
|
||||
<Heading2 className="!mt-0">Lorem ipsum</Heading2>
|
||||
<Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph>
|
||||
<Button onClick={() => navigate("/onboarding")}>Back</Button>
|
||||
<Button onClick={() => alert("Check proxy or smth")}>
|
||||
Check extension
|
||||
</Button>
|
||||
</CenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
}
|
22
src/pages/onboarding/onboardingHooks.ts
Normal file
22
src/pages/onboarding/onboardingHooks.ts
Normal file
|
@ -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 };
|
||||
}
|
|
@ -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() {
|
|||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||
<Route
|
||||
path="/onboarding/extension"
|
||||
element={<OnboardingExtensionPage />}
|
||||
/>
|
||||
<Route path="/onboarding/proxy" element={<OnboardingProxyPage />} />
|
||||
|
||||
{shouldHaveDmcaPage() ? (
|
||||
<Route path="/dmca" element={<DmcaPage />} />
|
||||
|
|
|
@ -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<string[]>;
|
||||
HAS_ONBOARDING: boolean;
|
||||
}
|
||||
|
||||
const env: Record<keyof Config, undefined | string> = {
|
||||
|
@ -49,6 +51,7 @@ const env: Record<keyof Config, undefined | string> = {
|
|||
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(",")
|
||||
|
|
|
@ -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]);
|
||||
|
|
22
src/stores/onboarding/index.tsx
Normal file
22
src/stores/onboarding/index.tsx
Normal file
|
@ -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<OnboardingStore>((set) => ({
|
||||
skipped: false,
|
||||
setSkipped(v) {
|
||||
set((s) => {
|
||||
s.skipped = v;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{ name: "__MW::onboarding" },
|
||||
),
|
||||
);
|
23
src/utils/onboarding.ts
Normal file
23
src/utils/onboarding.ts
Normal file
|
@ -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<boolean> {
|
||||
// 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;
|
||||
}
|
Loading…
Reference in a new issue