Merge pull request #275 from learnhouse/feat/onboarding

Init onboarding
This commit is contained in:
Badr B 2024-06-19 16:08:32 +01:00 committed by GitHub
commit a031f0ed17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 290 additions and 7 deletions

View file

@ -3,6 +3,7 @@ import { OrgProvider } from '@components/Contexts/OrgContext'
import NextTopLoader from 'nextjs-toploader';
import Toast from '@components/StyledElements/Toast/Toast'
import '@styles/globals.css'
import Onboarding from '@components/Onboarding/Onboarding';
export default function RootLayout({
children,
@ -16,6 +17,7 @@ export default function RootLayout({
<OrgProvider orgslug={params.orgslug}>
<NextTopLoader color="#2e2e2e" initialPosition={0.3} height={4} easing={'ease'} speed={500} showSpinner={false} />
<Toast />
<Onboarding />
{children}
</OrgProvider>
</div>

View file

@ -4,7 +4,6 @@ import { swrFetcher } from '@services/utils/ts/requests'
import React, { createContext, useContext, useEffect, useReducer } from 'react'
import useSWR from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
export const CourseContext = createContext(null)
export const CourseDispatchContext = createContext(null)

View file

@ -0,0 +1,277 @@
import Modal from '@components/StyledElements/Modal/Modal';
import Image, { StaticImageData } from 'next/image';
import React, { useEffect, useState } from 'react';
import OnBoardWelcome from '@public/onboarding/OnBoardWelcome.png';
import OnBoardCourses from '@public/onboarding/OnBoardCourses.png';
import OnBoardActivities from '@public/onboarding/OnBoardActivities.png';
import OnBoardEditor from '@public/onboarding/OnBoardEditor.png';
import OnBoardAI from '@public/onboarding/OnBoardAI.png';
import OnBoardUGs from '@public/onboarding/OnBoardUGs.png';
import OnBoardAccess from '@public/onboarding/OnBoardAccess.png';
import OnBoardMore from '@public/onboarding/OnBoardMore.png';
import { ArrowRight, Book, Check, Globe, Info, PictureInPicture, Sparkle, Sprout, SquareUser, Users } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { getUriWithOrg } from '@services/config/config';
import { useOrg } from '@components/Contexts/OrgContext';
import useAdminStatus from '@components/Hooks/useAdminStatus';
interface OnboardingStep {
imageSrc: StaticImageData;
title: string;
description: string;
buttons?: {
label: string;
action: () => void;
icon?: React.ReactNode;
}[];
}
const Onboarding: React.FC = () => {
const [currentStep, setCurrentStep] = useState(0);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isOnboardingComplete, setIsOnboardingComplete] = useState(true);
const router = useRouter();
const org = useOrg() as any;
const isUserAdmin = useAdminStatus() as any;
const onboardingData: OnboardingStep[] = [
{
imageSrc: OnBoardWelcome,
title: 'Teach the world!',
description: 'Welcome to LearnHouse, a LMS engineered for simplicity, ease of use and performance, meet the new way to create, share, and engage with educational content.',
},
{
imageSrc: OnBoardCourses,
title: 'Create Courses',
description: 'Courses are the main building blocks of LearnHouse, they always contain Chapters and Chapters contain Activities.',
buttons: [
{
label: 'Create New Course',
action: () => router.push(getUriWithOrg(org?.slug, '/courses?new=true')),
icon: <Book size={16} />,
},
],
},
{
imageSrc: OnBoardActivities,
title: 'Activities',
description: 'Activities are elements you can add to your Courses via Chapters, they can be : Dynamic Pages, Videos, Documents, Quizz and more soon.',
buttons: [
{
label: 'Learn more about activities',
action: () => window.open('https://university.learnhouse.io/course/be89716c-9992-44bb-81df-ef3d76e355ba', '_blank'),
icon: <Info size={16} />,
},
],
},
{
imageSrc: OnBoardEditor,
title: 'Dynamic pages and The Editor',
description: 'Dynamic pages are pages with dynamic content, like Notion pages they can contain various components like Quizzes, Images, Videos, Documents etc',
buttons: [
{
label: 'Learn more about Dynamic Pages and The Editor',
action: () => window.open('https://university.learnhouse.io/course/be89716c-9992-44bb-81df-ef3d76e355ba', '_blank'),
icon: <Info size={16} />,
},
],
},
{
imageSrc: OnBoardAI,
title: 'Artificial Intelligence',
description: 'Tools for tought made for teachers and students alike, context aware it can reply based on your courses and the unique content you create on LearnHouse',
buttons: [
{
label: 'Learn more about LearnHouse AI',
action: () => window.open('https://docs.learnhouse.app/features/ai/students', '_blank'),
icon: <Sparkle size={16} />,
},
],
},
{
imageSrc: OnBoardUGs,
title: 'Group students and streamline access ',
description: 'With UserGroups you can separate students by Groups and give access to Courses depending on their needs',
buttons: [
{
label: 'Create UserGroups',
action: () => router.push(getUriWithOrg(org?.slug, '/dash/users/settings/usergroups')),
icon: <SquareUser size={16} />,
},
],
},
{
imageSrc: OnBoardAccess,
title: 'Choose whether to make Courses available on the Web or not ',
description: 'You can choose to make your Courses discoverable from search engines and accesible to non authenticated users or to only give it to authenticated Users',
buttons: [
],
},
{
imageSrc: OnBoardMore,
title: 'To infinity and beyond',
description: "To Learn more about LearnHouse, you're welcome to follow our Original courses on the LearnHouse University",
buttons: [
{
label: 'LearnHouse University',
action: () => window.open('https://university.learnhouse.io', '_blank'),
icon: <Globe size={16} />,
},
],
},
];
useEffect(() => {
// Check if onboarding is already completed in local storage
const isOnboardingCompleted = localStorage.getItem('isOnboardingCompleted');
setIsOnboardingComplete(isOnboardingCompleted === 'true');
setIsModalOpen(!isOnboardingCompleted); // Show modal if onboarding is not completed
}, []);
const nextStep = () => {
if (currentStep < onboardingData.length - 1) {
setCurrentStep(currentStep + 1);
} else {
// Mark onboarding as completed in local storage
localStorage.setItem('isOnboardingCompleted', 'true');
setIsModalOpen(false); // Close modal after completion
setIsOnboardingComplete(true); // Show success message
console.log('Onboarding completed');
}
};
const skipOnboarding = () => {
// Mark onboarding as completed in local storage
localStorage.setItem('isOnboardingCompleted', 'true');
setIsModalOpen(false); // Close modal after skipping
console.log('Onboarding skipped');
};
const goToStep = (index: number) => {
if (index >= 0 && index < onboardingData.length) {
setCurrentStep(index);
}
};
return (
<div>
{isUserAdmin.isAdmin && !isUserAdmin.loading && !isOnboardingComplete && <Modal
isDialogOpen={isModalOpen}
onOpenChange={setIsModalOpen}
minHeight="sm"
minWidth='md'
dialogContent={
<OnboardingScreen
step={onboardingData[currentStep]}
onboardingData={onboardingData}
currentStep={currentStep}
nextStep={nextStep}
skipOnboarding={skipOnboarding}
setIsModalOpen={setIsModalOpen}
goToStep={goToStep}
/>
}
dialogTrigger={
<div className='fixed pb-10 w-full bottom-0 bg-gradient-to-t from-1% from-gray-950/25 to-transparent'>
<div className='bg-gray-950 flex space-x-2 font-bold cursor-pointer hover:bg-gray-900 shadow-md items-center text-gray-200 px-5 py-2 w-fit rounded-full mx-auto'>
<Sprout size={20} />
<p>Onboarding</p>
<div className='h-2 w-2 bg-green-500 animate-pulse rounded-full'></div>
</div>
</div>
}
/>}
</div>
);
};
interface OnboardingScreenProps {
step: OnboardingStep;
currentStep: number;
nextStep: () => void;
skipOnboarding: () => void;
goToStep: (index: number) => void;
setIsModalOpen: (value: boolean) => void;
onboardingData: OnboardingStep[];
}
const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
step,
currentStep,
nextStep,
skipOnboarding,
goToStep,
onboardingData,
setIsModalOpen,
}) => {
const isLastStep = currentStep === onboardingData.length - 1;
return (
<div className='flex flex-col'>
<div className='onboarding_screens flex-col px-4 py-4'>
<div className='flex-grow rounded-xl'>
<Image className='mx-auto shadow-md shadow-gray-200 rounded-lg aspect-auto' alt='' quality={100} src={step.imageSrc} />
</div>
<div className='grid grid-flow-col justify-stretch space-x-3 mt-4'>
{onboardingData.map((_, index) => (
<div
key={index}
onClick={() => goToStep(index)}
className={`h-[7px] w-auto ${index === currentStep ? 'bg-black' : 'bg-gray-300'} hover:bg-gray-700 rounded-lg shadow-md cursor-pointer`}
></div>
))}
</div>
</div>
<div className='onboarding_text flex flex-col h-[90px] py-2 px-4 leading-tight'>
<h2 className='text-xl font-bold'>{step.title}</h2>
<p className='text-md font-normal'>{step.description}</p>
</div>
<div className='onboarding_actions flex flex-row-reverse w-full px-4'>
<div className='flex flex-row justify-between w-full py-2'>
<div className='utils_buttons flex flex-row space-x-2'>
<div
className="inline-flex items-center px-5 space-x-1 cursor-pointer py-1 rounded-full text-gray-600 antialiased font-bold bg-gray-100 hover:bg-gray-200"
onClick={() => setIsModalOpen(false)}
>
<PictureInPicture size={16} />
</div>
</div>
<div className='actions_buttons flex space-x-2'>
{step.buttons?.map((button, index) => (
<div
key={index}
className="inline-flex items-center px-5 space-x-2 cursor-pointer py-1 rounded-full text-gray-200 antialiased font-bold bg-black hover:bg-gray-700 shadow-md whitespace-nowrap"
onClick={button.action}
>
<p>{button.label}</p>
{button.icon}
</div>
))}
{isLastStep ? (
<div
className="inline-flex items-center px-5 space-x-2 cursor-pointer py-1 rounded-full text-gray-200 antialiased font-bold bg-black hover:bg-gray-700 shadow-md whitespace-nowrap"
onClick={nextStep}
>
<p>Finish Onboarding</p>
<Check size={16} />
</div>
) : (
<div
className="inline-flex items-center px-5 space-x-2 cursor-pointer py-1 rounded-full text-gray-200 antialiased font-bold bg-black hover:bg-gray-700 shadow-md whitespace-nowrap"
onClick={nextStep}
>
<p>Next</p>
<ArrowRight size={16} />
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default Onboarding;

View file

@ -6,8 +6,8 @@ import { blackA, mauve } from '@radix-ui/colors'
import { ButtonBlack } from '../Form/Form'
type ModalParams = {
dialogTitle: string
dialogDescription: string
dialogTitle?: string
dialogDescription?: string
dialogContent: React.ReactNode
dialogClose?: React.ReactNode | null
dialogTrigger?: React.ReactNode
@ -16,6 +16,8 @@ type ModalParams = {
isDialogOpen?: boolean
minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
minWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
customHeight?: string
customWidth?: string
}
const Modal = (params: ModalParams) => (
@ -30,11 +32,14 @@ const Modal = (params: ModalParams) => (
className="overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full"
minHeight={params.minHeight}
minWidth={params.minWidth}
>
<DialogTopBar className="-space-y-1">
<DialogTitle>{params.dialogTitle}</DialogTitle>
<DialogDescription>{params.dialogDescription}</DialogDescription>
</DialogTopBar>
{params.dialogTitle && params.dialogDescription &&
<DialogTopBar className="-space-y-1">
<DialogTitle>{params.dialogTitle}</DialogTitle>
<DialogDescription>{params.dialogDescription}</DialogDescription>
</DialogTopBar>
}
{params.dialogContent}
{params.dialogClose ? (
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB