13 changed files with 281 additions and 3 deletions
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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 }; |
||||||
|
} |
@ -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" }, |
||||||
|
), |
||||||
|
); |
@ -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 new issue