Skip to content

Commit

Permalink
Add onboarding task list (#512)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellno authored Sep 11, 2024
1 parent 60448c5 commit 239da6b
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 43 deletions.
3 changes: 3 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
},
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"utils": "@/lib/utils"
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.1.1",
Expand Down
2 changes: 1 addition & 1 deletion pages/welcome/connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const ConnectAccountPage = () => {
return (
<div className="mx-auto flex flex-col justify-center items-center">
<div className="space-y-6 p-10 pb-16 block text-center">
<h2 className="text-4xl font-bold tracking-tight">Welcome to herocast</h2>
<h2 className="text-4xl font-bold tracking-tight">Welcome to herocast</h2>
<p className="text-lg text-muted-foreground">Build, engage and grow on Farcaster. Faster.</p>
<div className="lg:max-w-lg mx-auto">
<div className="grid grid-cols-1 gap-4">
Expand Down
4 changes: 2 additions & 2 deletions pages/welcome/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const CreateAccountPage = () => {
return (
<div className="w-full flex flex-col mt-24 items-center">
<div className="space-y-6 p-10 pb-16 block text-center">
<h2 className="text-4xl font-bold tracking-tight">Welcome to herocast</h2>
<h2 className="text-4xl font-bold tracking-tight">Welcome to herocast</h2>
<p className="text-lg text-muted-foreground">Build, engage and grow on Farcaster. Faster.</p>
<div className="lg:max-w-lg mx-auto">
<Card>
Expand All @@ -70,7 +70,7 @@ const CreateAccountPage = () => {
<CardFooter>
<Button className="w-full" variant="default" onClick={onCreateNewAccount}>
<UserPlusIcon className="mr-1.5 h-5 w-5" aria-hidden="true" />
{isLoading ? 'Creating account...' : 'Get Started'}
{isLoading ? 'Loading...' : 'Get Started'}
</Button>
</CardFooter>
</Card>
Expand Down
220 changes: 195 additions & 25 deletions pages/welcome/success.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
CheckCircleIcon,
MagnifyingGlassIcon,
Expand All @@ -13,55 +13,225 @@ import { useRouter } from 'next/router';
import { useDraftStore } from '@/stores/useDraftStore';
import { JoinedHerocastPostDraft } from '@/common/constants/postDrafts';
import Link from 'next/link';
import findIndex from 'lodash.findindex';
import { Progress } from '@/components/ui/progress';
import { Checkbox } from '@/components/ui/checkbox';
import { DraftStatus } from '@/common/constants/farcaster';
import { useAccountStore } from '@/stores/useAccountStore';
import { useListStore } from '@/stores/useListStore';
import { LOCAL_STORAGE_ONBOARDING_COMPLETED_KEY } from '@/common/constants/localStorage';

enum OnboardingStep {
login_to_herocast = 'login_to_herocast',
connect_farcaster_account = 'connect_farcaster_account',
create_keyword_alert = 'create_keyword_alert',
pin_channels = 'pin_channels',
schedule_cast = 'schedule_cast',
done = 'done',
}

interface OnboardingTaskStatus {
[key: string]: boolean;
}

const onboardingSteps = [
{
key: OnboardingStep.login_to_herocast,
title: 'Login to herocast',
},
{
key: OnboardingStep.connect_farcaster_account,
title: 'Connect Farcaster account',
},
{
key: OnboardingStep.create_keyword_alert,
title: 'Create keyword alerts and daily email',
},
{
key: OnboardingStep.pin_channels,
title: 'Pin channels',
},
{
key: OnboardingStep.schedule_cast,
title: 'Schedule casts',
},
{
key: OnboardingStep.done,
title: 'Done',
hide: true,
},
];

const WelcomeSuccessPage = () => {
const router = useRouter();
const { addNewPostDraft } = useDraftStore();
const { drafts, addNewPostDraft } = useDraftStore();
const [step, setStep] = useState<OnboardingStep>(OnboardingStep.create_keyword_alert);
const [taskStatus, setTaskStatus] = useState<OnboardingTaskStatus>({
[OnboardingStep.login_to_herocast]: true,
[OnboardingStep.connect_farcaster_account]: true,
[OnboardingStep.create_keyword_alert]: false,
[OnboardingStep.pin_channels]: false,
[OnboardingStep.schedule_cast]: false,
});

const onStartCasting = () => {
addNewPostDraft(JoinedHerocastPostDraft);
router.push('/post');
};

const hasPinnedChannels = useAccountStore((state) => state.accounts.some((account) => account.channels.length > 0));
const hasScheduledCasts =
drafts.filter((draft) => draft.status === DraftStatus.scheduled || draft.status === DraftStatus.published).length >
0;
const hasSavedSearches = useListStore((state) => state.lists.length > 0);

useEffect(() => {
if (hasScheduledCasts) {
setTaskStatus((prev) => ({ ...prev, [OnboardingStep.schedule_cast]: true }));
}
}, [hasScheduledCasts]);

useEffect(() => {
if (hasPinnedChannels) {
setTaskStatus((prev) => ({ ...prev, [OnboardingStep.pin_channels]: true }));
}
}, [hasPinnedChannels]);

useEffect(() => {
if (hasSavedSearches) {
setTaskStatus((prev) => ({ ...prev, [OnboardingStep.create_keyword_alert]: true }));
}
}, [hasSavedSearches]);

const progressPercent = (Object.values(taskStatus).filter(Boolean).length / (onboardingSteps.length - 1)) * 100;
const isCompleted = Object.values(taskStatus).every(Boolean);

useEffect(() => {
if (isCompleted) {
localStorage.setItem(LOCAL_STORAGE_ONBOARDING_COMPLETED_KEY, 'true');
// dispatch event to notify all open tabs (including the one setting it)
const event = new StorageEvent('storage', {
key: LOCAL_STORAGE_ONBOARDING_COMPLETED_KEY,
oldValue: undefined,
newValue: 'true',
});
window.dispatchEvent(event);
}
}, [isCompleted]);

const renderOnboardingSteps = () => {
return onboardingSteps
.filter((step) => !step?.hide)
.map((step, idx) => {
return (
<div key={step.key} className="flex items-center justify-between">
<div className="flex items-center align-middle">
<div className="flex-shrink-0 mr-2">
{taskStatus[step.key] ? (
<CheckCircleIcon className="h-6 w-6 text-green-600" aria-hidden="true" />
) : (
<Checkbox checked={false} className="cursor-default ml-0.5 mt-1 h-5 w-5 rounded-lg" />
)}
</div>
{step.title}
{!taskStatus[step.key] && (
<Button
variant="outline"
size="sm"
className="h-6 px-2 ml-2"
onClick={() => {
setTaskStatus((prev) => {
const newStatus = { ...prev, [step.key]: true };
return newStatus;
});
setStep(onboardingSteps[idx + 1]?.key);
}}
>
Skip
</Button>
)}
</div>
</div>
);
});
};

const renderGloPromoCard = () => {
return (
<Card className="bg-background text-foreground">
<CardHeader className="pb-0">
<CardTitle className="text-lg font-semibold">
<img
src="https://github.com/hero-org/.github/blob/main/assets/IMAGE%202024-06-13%2013:12:57.jpg?raw=true"
className="h-10 -ml-1"
/>
Get 2 USDGLO
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<CardDescription className="text-lg text-card-foreground">
Create a saved search and schedule a cast to get 2 USDGLO. <br />
Glo Dollar is a fiat-backed stablecoin that funds public goods. <br />
This is a limited time offer until Dec 12, 2024 or until our budget is depleted.
<br />
<br />
<a
href="https://www.glodollar.org/articles/how-glo-dollar-works"
target="_blank"
rel="noreferrer"
className="hover:underline"
>
Learn more →
</a>
</CardDescription>
</CardContent>
</Card>
);
};

return (
<div className="w-full flex flex-col mt-24 items-center">
<div className="space-y-6 p-10 pb-16 block text-center">
<h2 className="text-4xl font-bold tracking-tight">Welcome to herocast</h2>
<div className="max-w-xl mx-auto">
<Card className="min-w-max bg-background text-foreground">
<CardHeader className="space-y-1">
<CardTitle className="flex">
<CheckCircleIcon className="-mt-0.5 mr-1 h-5 w-5 text-foreground/80" aria-hidden="true" />
Account added to herocast
</CardTitle>
</CardHeader>
<CardContent>
<h2 className="text-4xl font-bold tracking-tight">Welcome to herocast ✨</h2>
<div className="max-w-max mx-auto">
<Card className="bg-background text-foreground">
<CardContent className="max-w-2xl p-4">
<div className="flex flex-col gap-y-4 text-left">
<div>
<span className="text-md font-semibold">Get started with herocast</span>
<ul className="ml-1 list-disc list-inside">
<li>Create an alert to get notified when someone mentions a keyword</li>
<li>Pin Channels to access them faster in your Feeds</li>
<li>Schedule casts to save time</li>
</ul>
<span className="text-md font-semibold">
{isCompleted ? 'Enjoy herocast' : 'Complete these tasks to get the most out of herocast'}
</span>
</div>
<div className="gap-x-4 mt-2 flex">
<div className="space-y-4 py-4 block">{renderOnboardingSteps()}</div>
<Progress
value={progressPercent}
indicatorClassName="bg-gradient-to-r from-green-400 to-green-600 animate-pulse"
/>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mt-2">
<Link href="/search">
<Button size="lg" type="button" variant="default">
<Button size="lg" type="button" variant="default" className="min-w-full px-2">
<MagnifyingGlassIcon className="mr-1.5 mt-0.5 h-4 w-4" aria-hidden="true" />
Setup a keyword alert
Create keyword alerts
</Button>
</Link>
<Link href="/channels">
<Button size="lg" type="button" variant="outline">
<Button size="lg" type="button" variant="outline" className="min-w-full px-2">
<RectangleGroupIcon className="mr-1.5 mt-0.5 h-4 w-4" aria-hidden="true" />
Pin your channels
Pin channels
</Button>
</Link>
<Button onClick={() => onStartCasting()} type="button" variant="outline" size="lg">
<Button
onClick={() => onStartCasting()}
type="button"
variant="outline"
size="lg"
className="min-w-full px-2"
>
<PencilSquareIcon className="mr-1.5 mt-0.5 h-4 w-4" aria-hidden="true" />
Start casting
Schedule casts
</Button>
</div>
{renderGloPromoCard()}
</div>
</CardContent>
</Card>
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/CastRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ export const CastRow = ({
<div
className={cn(
cast.embeds?.length > 1 && !embedsContainsCastEmbed && 'grid lg:grid-cols-2 gap-4',
'max-w-lg self-start space-y-2'
'max-w-lg self-start'
)}
onClick={(e) => e.preventDefault()}
>
Expand Down
46 changes: 34 additions & 12 deletions src/common/components/Editor/NewCastEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { creationMods } from '@mod-protocol/mod-registry';
import { renderers } from '@mod-protocol/react-ui-shadcn/dist/renderers';
import map from 'lodash.map';
import { renderEmbedForUrl } from '../Embeds';
import { PhotoIcon } from '@heroicons/react/20/solid';
import { CalendarDaysIcon, PhotoIcon } from '@heroicons/react/20/solid';
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
import { Channel } from '@neynar/nodejs-sdk/build/neynar-api/v2';
import { ChannelList } from '../ChannelList';
Expand Down Expand Up @@ -91,6 +91,13 @@ export default function NewPostEntry({

const hasEmbeds = draft?.embeds && !!draft.embeds.length;
const account = useAccountStore((state) => state.accounts[state.selectedAccountIdx]);
const hasMultipleActiveAccounts =
useAccountStore(
(state) =>
state.accounts.filter((account) => {
return account.status === 'active';
}).length
) > 1;
const { allChannels } = useAccountStore();
const isReply = draft?.parentCastId !== undefined;

Expand Down Expand Up @@ -281,7 +288,7 @@ export default function NewPostEntry({
const getButtonText = () => {
if (isPublishing) return scheduleDateTime ? 'Scheduling...' : 'Publishing...';

return `${scheduleDateTime ? 'Schedule' : 'Cast'}${account ? ` as ${account.name}` : ''}`;
return `${scheduleDateTime ? 'Schedule' : 'Cast'}${account && hasMultipleActiveAccounts ? ` as ${account.name}` : ''}`;
};

const scheduledCastCount =
Expand Down Expand Up @@ -362,21 +369,36 @@ export default function NewPostEntry({
</PopoverContent>
</Popover>
{textLengthWarning && <div className={cn('my-2 ml-2 text-sm', textLengthTailwind)}>{textLengthWarning}</div>}
<div className="grow"></div>
{onRemove && (
<Button className="h-9" variant="outline" type="button" onClick={onRemove} disabled={isPublishing}>
Remove
</Button>
)}
{!hideSchedule && (
<DateTimePicker
granularity="minute"
hourCycle={24}
jsDate={scheduleDateTime}
onJsDateChange={setScheduleDateTime}
showClearButton
/>
)}
{!hideSchedule &&
(scheduleDateTime ? (
<DateTimePicker
granularity="minute"
hourCycle={24}
jsDate={scheduleDateTime}
onJsDateChange={setScheduleDateTime}
showClearButton
/>
) : (
<Button
className="h-9"
type="button"
variant="outline"
disabled={isPublishing}
onClick={() => {
const date = new Date();
date.setDate(date.getDate() + 1);
setScheduleDateTime(date);
}}
>
<CalendarDaysIcon className="mr-1 w-5 h-5" />
Schedule
</Button>
))}
</div>
<div className="flex flex-row pt-2 justify-between">
<div>
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/Sidebar/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const RightSidebar = ({ showFeeds, showSearches, showLists, showManageLists, sho

return (
<>
<div className="pt-16 mx-4">
<div className="mt-16 mx-4">
<ProfileInfo
fid={selectedCast.author.fid}
viewerFid={Number(selectedAccount.platformAccountId)}
Expand Down
1 change: 1 addition & 0 deletions src/common/constants/localStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const LOCAL_STORAGE_ONBOARDING_COMPLETED_KEY = 'herocastOnboardingCompleted';
Loading

0 comments on commit 239da6b

Please sign in to comment.