Skip to content
131 changes: 69 additions & 62 deletions apps/desktop/src/components/main/body/onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,72 +117,79 @@ export function TabContentOnboarding({

return (
<StandardTabWrapper>
<div className="relative h-full overflow-y-auto">
<button
onClick={() => setIsMuted((prev) => !prev)}
className="sticky top-2 float-right mr-2 p-1.5 rounded-full hover:bg-neutral-100 transition-colors z-10"
aria-label={isMuted ? "Unmute" : "Mute"}
>
{isMuted ? (
<VolumeXIcon size={16} className="text-neutral-600" />
) : (
<Volume2Icon size={16} className="text-neutral-600" />
)}
</button>

<div className="flex flex-col px-6 pt-4 pb-16 gap-8">
<div className="relative flex h-full flex-col">
<div className="sticky top-0 z-10 flex items-center justify-between bg-white px-6 pt-4 pb-3">
<h1 className="text-2xl font-semibold font-serif text-neutral-900">
Welcome to Char
</h1>

<OnboardingSection
title="Permissions"
description="Required for best experience"
status={getStepStatus("permissions", currentStep)}
onBack={goBack}
onNext={goNext}
>
<PermissionsSection />
</OnboardingSection>

<OnboardingSection
title="Account"
description="Sign in to unlock Pro features"
status={getStepStatus("login", currentStep)}
onBack={goBack}
onNext={goNext}
>
<LoginSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Calendar"
description="Select calendars to sync"
status={getStepStatus("calendar", currentStep)}
onBack={goBack}
onNext={goNext}
>
<CalendarSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Storage"
description="Where your notes and recordings are stored"
status={getStepStatus("folder-location", currentStep)}
onBack={goBack}
onNext={goNext}
<button
onClick={() => setIsMuted((prev) => !prev)}
className="p-1.5 rounded-full hover:bg-neutral-100 transition-colors"
aria-label={isMuted ? "Unmute" : "Mute"}
>
<FolderLocationSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Ready to go"
status={getStepStatus("final", currentStep)}
onBack={goBack}
onNext={() => void finishOnboarding(handleFinish)}
>
<FinalSection onContinue={handleFinish} />
</OnboardingSection>
{isMuted ? (
<VolumeXIcon size={16} className="text-neutral-600" />
) : (
<Volume2Icon size={16} className="text-neutral-600" />
)}
</button>
</div>

<div className="flex-1 overflow-y-auto">
<div className="flex flex-col px-6 pb-16 gap-3">
<OnboardingSection
title="Permissions"
completedTitle="Permissions granted"
description="Required for best experience"
status={getStepStatus("permissions", currentStep)}
onBack={goBack}
onNext={goNext}
>
<PermissionsSection />
</OnboardingSection>

<OnboardingSection
title="Account"
description="Sign in to unlock Pro features"
completedTitle="Signed up"
status={getStepStatus("login", currentStep)}
onBack={goBack}
onNext={goNext}
>
<LoginSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Calendar"
description="Select calendars to sync"
completedTitle="Calendar connected"
status={getStepStatus("calendar", currentStep)}
onBack={goBack}
onNext={goNext}
>
<CalendarSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Storage"
description="Where your notes and recordings are stored"
completedTitle="Storage configured"
status={getStepStatus("folder-location", currentStep)}
onBack={goBack}
onNext={goNext}
>
<FolderLocationSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Ready to go"
status={getStepStatus("final", currentStep)}
onBack={goBack}
onNext={() => void finishOnboarding(handleFinish)}
>
<FinalSection onContinue={handleFinish} />
</OnboardingSection>
</div>
</div>
</div>
</StandardTabWrapper>
Expand Down
62 changes: 18 additions & 44 deletions apps/desktop/src/components/onboarding/account/before-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,32 @@ import { useEffect, useState } from "react";
import { useAuth } from "../../../auth";
import { OnboardingButton } from "../shared";

export function BeforeLogin({ onContinue }: { onContinue: () => void }) {
return (
<div className="flex flex-col gap-4">
<SigninButton />
<ControlRegion handleContinue={onContinue} />
</div>
);
}

function SigninButton() {
const auth = useAuth();

const triggered = useAutoTriggerSignin();

return (
<OnboardingButton onClick={() => auth?.signIn()} disabled={triggered}>
{triggered ? "Click here to Sign in" : "Signing in on your browser..."}
</OnboardingButton>
);
}

function ControlRegion(_: { handleContinue: () => void }) {
export function BeforeLogin() {
const auth = useAuth();
const autoSignInCompleted = useAutoTriggerSignin();
const [showCallbackUrlInput, setShowCallbackUrlInput] = useState(false);

return (
<div className="flex flex-col gap-4">
{showCallbackUrlInput ? <CallbackUrlInput /> : null}

<div className="flex flex-row gap-2 items-center mx-auto">
<button
className="text-sm text-neutral-500 hover:text-neutral-600 underline"
<div className="flex items-center gap-3">
<OnboardingButton
onClick={() => auth?.signIn()}
disabled={!autoSignInCompleted}
>
Browser not opened?
</button>

<span className="text-sm text-neutral-400 mx-1">/</span>
<button
className="text-sm text-neutral-500 hover:text-neutral-600 underline"
onClick={(_v) => setShowCallbackUrlInput(true)}
>
Deeplink not working?
</button>
{/* <span className="text-sm text-neutral-600">or </span>
<button
className="text-sm text-neutral-400 hover:text-neutral-600 underline"
onClick={() => handleContinue()}
>
continue without account.
</button> */}
{autoSignInCompleted
? "Click here to Sign in"
: "Signing in on your browser..."}
</OnboardingButton>
{autoSignInCompleted && (
<button
className="text-sm text-neutral-500 hover:text-neutral-600 underline"
onClick={() => setShowCallbackUrlInput(true)}
>
Something not working?
</button>
)}
</div>
{showCallbackUrlInput && <CallbackUrlInput />}
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/components/onboarding/account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export function LoginSection({ onContinue }: { onContinue: () => void }) {
return <AfterLogin onContinue={onContinue} />;
}

return <BeforeLogin onContinue={onContinue} />;
return <BeforeLogin />;
}
42 changes: 29 additions & 13 deletions apps/desktop/src/components/onboarding/final.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Icon } from "@iconify-icon/react";

import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { commands as openerCommands } from "@hypr/plugin-opener2";
import { commands as sfxCommands } from "@hypr/plugin-sfx";
Expand All @@ -6,29 +8,43 @@ import { commands } from "../../types/tauri.gen";
import { OnboardingButton } from "./shared";

const SOCIALS = [
{ label: "Discord", url: "https://discord.gg/CX8gTH2tj9" },
{ label: "GitHub", url: "https://github.com/fastrepl/hyprnote" },
{ label: "X", url: "https://x.com/getcharnotes" },
{
label: "Discord",
icon: "simple-icons:discord",
size: 14,
url: "https://discord.gg/CX8gTH2tj9",
},
{
label: "GitHub",
icon: "simple-icons:github",
size: 14,
url: "https://github.com/fastrepl/hyprnote",
},
{
label: "X",
icon: "simple-icons:x",
size: 10,
url: "https://x.com/getcharnotes",
},
] as const;

export function FinalSection({ onContinue }: { onContinue: () => void }) {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<p className="text-sm text-neutral-500">
Join our community and stay updated:
</p>
<div className="flex gap-2">
{SOCIALS.map(({ label, url }) => (
<div className="flex items-center gap-1.5 text-sm text-neutral-500">
<span>Join our community and stay updated:</span>
{SOCIALS.map(({ label, icon, size, url }) => {
return (
<button
key={label}
onClick={() => void openerCommands.openUrl(url, null)}
className="px-4 py-2 text-sm rounded-full border border-neutral-200 text-neutral-700 transition-colors duration-150 hover:bg-neutral-50 hover:border-neutral-300"
className="inline-flex items-center justify-center size-6 rounded-md text-neutral-400 transition-colors duration-150 hover:text-neutral-700"
aria-label={label}
>
{label}
<Icon icon={icon} width={size} height={size} />
</button>
))}
</div>
);
})}
</div>

<OnboardingButton onClick={() => void finishOnboarding(onContinue)}>
Expand Down
47 changes: 35 additions & 12 deletions apps/desktop/src/components/onboarding/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,72 @@ import {
XCircleIcon,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import type { ReactNode } from "react";
import { type ReactNode, useEffect, useRef } from "react";

import { cn } from "@hypr/utils";

const SCROLL_DELAY_MS = 350;

export type SectionStatus = "completed" | "active" | "upcoming";

export function OnboardingSection({
title,
completedTitle,
description,
status,
onBack,
onNext,
children,
}: {
title: string;
completedTitle?: string;
description?: string;
status: SectionStatus | null;
onBack?: () => void;
onNext?: () => void;
children: ReactNode;
}) {
if (!status) return null;
const sectionRef = useRef<HTMLElement>(null);

const isActive = status === "active";
const isCompleted = status === "completed";

useEffect(() => {
if (!isActive) return;
const timeout = setTimeout(() => {
sectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}, SCROLL_DELAY_MS);
return () => clearTimeout(timeout);
}, [isActive]);

if (!status || status === "upcoming") return null;

return (
<section>
<section ref={sectionRef}>
<div
className={cn([
"flex items-center gap-2 mb-4 transition-all duration-300",
status === "upcoming" && "opacity-15",
isCompleted && "opacity-25",
"flex items-center gap-2 transition-all duration-300",
isActive && "mb-4",
])}
>
{isCompleted && (
<CheckIcon
className="size-4 shrink-0 text-green-600"
strokeWidth={2.5}
/>
)}
<div className="flex min-w-0 flex-col gap-1">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold font-serif text-neutral-900">
{title}
<h2
className={cn([
"transition-all duration-300",
isCompleted
? "text-sm font-normal text-neutral-300"
: "text-lg font-semibold font-serif text-neutral-900",
])}
>
{isCompleted ? (completedTitle ?? title) : title}
</h2>
{isCompleted && (
<CheckIcon className="size-3.5 text-neutral-900" aria-hidden />
)}
{import.meta.env.DEV && isActive && (onBack || onNext) && (
<div className="flex items-center gap-2">
{onBack && (
Expand Down Expand Up @@ -103,7 +126,7 @@ export function OnboardingButton(
return (
<button
{...props}
className="w-full py-3 rounded-full bg-stone-600 text-white text-sm font-medium duration-150 hover:scale-[1.01] active:scale-[0.99]"
className="w-fit px-6 py-2.5 rounded-full bg-stone-600 text-white text-sm font-medium duration-150 hover:scale-[1.01] active:scale-[0.99]"
/>
);
}
Expand Down
Loading