From 95a3105038269dfa9679ffe9e12e50c3bac3778a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 14 May 2024 14:15:30 +0700 Subject: [PATCH] feat: added onboarding --- .../src/api/routes/integrations.controller.ts | 2 +- apps/frontend/public/success.svg | 50 +++++ .../launches/add.provider.component.tsx | 12 +- .../launches/launches.component.tsx | 8 +- .../src/components/layout/layout.settings.tsx | 2 + .../components/layout/settings.component.tsx | 81 ++++--- .../onboarding/connect.channels.tsx | 203 +++++++++++++++++ .../onboarding/github.onboarding.tsx | 39 ++++ .../src/components/onboarding/onboarding.tsx | 211 ++++++++++++++++++ .../components/settings/github.component.tsx | 55 +---- 10 files changed, 585 insertions(+), 78 deletions(-) create mode 100644 apps/frontend/public/success.svg create mode 100644 apps/frontend/src/components/onboarding/connect.channels.tsx create mode 100644 apps/frontend/src/components/onboarding/github.onboarding.tsx create mode 100644 apps/frontend/src/components/onboarding/onboarding.tsx diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index ee11cff4..21d47e8d 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -24,7 +24,6 @@ export class IntegrationsController { private _integrationService: IntegrationService ) {} @Get('/') - @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) getIntegration() { return this._integrationManager.getAllIntegrations(); } @@ -56,6 +55,7 @@ export class IntegrationsController { } @Get('/social/:integration') + @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) async getIntegrationUrl(@Param('integration') integration: string) { if ( !this._integrationManager diff --git a/apps/frontend/public/success.svg b/apps/frontend/public/success.svg new file mode 100644 index 00000000..eef8857d --- /dev/null +++ b/apps/frontend/public/success.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index 795de7cf..68798e2e 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -44,8 +44,9 @@ export const ApiModal: FC<{ identifier: string; name: string; update?: () => void; + close?: () => void; }> = (props) => { - const { update, name } = props; + const { update, name, close: closePopup } = props; const fetch = useFetch(); const router = useRouter(); const modal = useModals(); @@ -55,6 +56,9 @@ export const ApiModal: FC<{ }); const close = useCallback(() => { + if (closePopup) { + return closePopup(); + } modal.closeAll(); }, []); @@ -68,7 +72,11 @@ export const ApiModal: FC<{ ); if (add.ok) { - modal.closeAll(); + if (closePopup) { + closePopup(); + } else { + modal.closeAll(); + } router.refresh(); if (update) update(); return; diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index 00f59a25..dd3f89f0 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -1,7 +1,7 @@ 'use client'; import { AddProviderButton } from '@gitroom/frontend/components/launches/add.provider.component'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import Image from 'next/image'; import { orderBy } from 'lodash'; import { Calendar } from '@gitroom/frontend/components/launches/calendar'; @@ -56,6 +56,12 @@ export const LaunchesComponent = () => { } }, []); + useEffect(() => { + if (typeof window !== 'undefined' && window.opener) { + window.close(); + } + }, []); + if (isLoading || reload) { return ; } diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 14b1fd50..7eaa9fc5 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -22,6 +22,7 @@ import isoWeek from 'dayjs/plugin/isoWeek'; import isBetween from 'dayjs/plugin/isBetween'; import { ShowLinkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component'; import { SettingsComponent } from '@gitroom/frontend/components/layout/settings.component'; +import { Onboarding } from '@gitroom/frontend/components/onboarding/onboarding'; dayjs.extend(utc); dayjs.extend(weekOfYear); @@ -50,6 +51,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { +
diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index 1e560cd7..3aed50bb 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -1,5 +1,5 @@ import { useModals } from '@mantine/modals'; -import React, { FC, useCallback, useEffect, useMemo } from 'react'; +import React, { FC, Ref, useCallback, useEffect, useMemo } from 'react'; import { Input } from '@gitroom/react/form/input'; import { Button } from '@gitroom/react/form/button'; import { Textarea } from '@gitroom/react/form/textarea'; @@ -10,8 +10,10 @@ import { classValidatorResolver } from '@hookform/resolvers/class-validator'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; import { useToaster } from '@gitroom/react/toaster/toaster'; import { useSWRConfig } from 'swr'; +import clsx from 'clsx'; -const SettingsPopup: FC = () => { +export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { + const { getRef } = props; const fetch = useFetch(); const toast = useToaster(); const swr = useSWRConfig(); @@ -48,6 +50,11 @@ const SettingsPopup: FC = () => { method: 'POST', body: JSON.stringify(val), }); + + if (getRef) { + return ; + } + toast.show('Profile updated'); swr.mutate('/marketplace/account'); close(); @@ -60,28 +67,38 @@ const SettingsPopup: FC = () => { return (
-
- } +
+ {!getRef && ( + -
Profile Settings
+ + + + + )} + {!getRef && ( +
Profile Settings
+ )}
Profile
@@ -106,7 +123,10 @@ const SettingsPopup: FC = () => {
Profile Picture
- -
-
- -
+ {!getRef && ( +
+ +
+ )}
diff --git a/apps/frontend/src/components/onboarding/connect.channels.tsx b/apps/frontend/src/components/onboarding/connect.channels.tsx new file mode 100644 index 00000000..25058f32 --- /dev/null +++ b/apps/frontend/src/components/onboarding/connect.channels.tsx @@ -0,0 +1,203 @@ +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { orderBy } from 'lodash'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import clsx from 'clsx'; +import Image from 'next/image'; +import { Menu } from '@gitroom/frontend/components/launches/menu/menu'; +import { ApiModal } from '@gitroom/frontend/components/launches/add.provider.component'; + +export const ConnectChannels: FC = () => { + const fetch = useFetch(); + const [identifier, setIdentifier] = useState(undefined); + + const getIntegrations = useCallback(async () => { + return (await fetch('/integrations')).json(); + }, []); + + const [reload, setReload] = useState(false); + + const getSocialLink = useCallback( + (identifier: string) => async () => { + const { url } = await ( + await fetch('/integrations/social/' + identifier) + ).json(); + + window.open(url, 'Social Connect', 'width=700,height=700'); + }, + [] + ); + + const load = useCallback(async (path: string) => { + return (await (await fetch(path)).json()).integrations; + }, []); + + const { data: integrations, mutate } = useSWR('/integrations/list', load, { + fallbackData: [], + }); + + const user = useUser(); + + const totalNonDisabledChannels = useMemo(() => { + return ( + integrations?.filter((integration: any) => !integration.disabled) + ?.length || 0 + ); + }, [integrations]); + + const sortedIntegrations = useMemo(() => { + return orderBy( + integrations, + ['type', 'disabled', 'identifier'], + ['desc', 'asc', 'asc'] + ); + }, [integrations]); + + const update = useCallback(async (shouldReload: boolean) => { + if (shouldReload) { + setReload(true); + } + await mutate(); + + if (shouldReload) { + setReload(false); + } + }, []); + + const finishUpdate = useCallback(() => { + setIdentifier(undefined); + update(true); + }, []); + + const { data } = useSWR('get-all-integrations', getIntegrations); + + return ( + <> + {!!identifier && ( +
+
+ setIdentifier(undefined)} + update={finishUpdate} + identifier={identifier.identifier} + name={identifier.name} + /> +
+
+ )} +
+
+
Connect Channels
+
+ Connect your social media and publishing websites channels to + schedule posts later +
+
+
+
+
Social
+
+ {data?.social.map((social: any) => ( +
+
+ +
+
+ {social.name} +
+
+ ))} +
+
+
+
Publishing Platforms
+
+ {data?.article.map((article: any) => ( +
setIdentifier(article)} + key={article.identifier} + className="h-[96px] bg-input flex flex-col justify-center items-center gap-[10px] cursor-pointer" + > +
+ +
+
+ {article.name} +
+
+ ))} +
+
+
+
+
+ {sortedIntegrations.length === 0 && ( +
No channels
+ )} + {sortedIntegrations.map((integration) => ( +
+
+ {integration.identifier} + {integration.identifier} +
+
+ {integration.name} +
+ totalNonDisabledChannels && + integration.disabled + } + canDisable={!integration.disabled} + /> +
+ ))} +
+
+
+ + ); +}; diff --git a/apps/frontend/src/components/onboarding/github.onboarding.tsx b/apps/frontend/src/components/onboarding/github.onboarding.tsx new file mode 100644 index 00000000..1e8332f5 --- /dev/null +++ b/apps/frontend/src/components/onboarding/github.onboarding.tsx @@ -0,0 +1,39 @@ +import { FC, useCallback } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { GithubComponent } from '@gitroom/frontend/components/settings/github.component'; + +export const GithubOnboarding: FC = () => { + const fetch = useFetch(); + + const load = useCallback(async (path: string) => { + const { github } = await (await fetch('/settings/github')).json(); + if (!github) { + return false; + } + + const emptyOnes = github.find((p: { login: string }) => !p.login); + const { organizations } = emptyOnes + ? await (await fetch(`/settings/organizations/${emptyOnes.id}`)).json() + : { organizations: [] }; + + return { github, organizations }; + }, []); + + const { isLoading: isLoadingSettings, data: loadAll, mutate } = useSWR( + 'load-all', + load + ); + + if (!loadAll) { + return null; + } + + return ( + + ); +}; diff --git a/apps/frontend/src/components/onboarding/onboarding.tsx b/apps/frontend/src/components/onboarding/onboarding.tsx new file mode 100644 index 00000000..ea25c718 --- /dev/null +++ b/apps/frontend/src/components/onboarding/onboarding.tsx @@ -0,0 +1,211 @@ +'use client'; + +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useModals } from '@mantine/modals'; +import clsx from 'clsx'; +import { GithubOnboarding } from '@gitroom/frontend/components/onboarding/github.onboarding'; +import { SettingsPopup } from '@gitroom/frontend/components/layout/settings.component'; +import { Button } from '@gitroom/react/form/button'; +import { ConnectChannels } from '@gitroom/frontend/components/onboarding/connect.channels'; + +const Step: FC<{ step: number; title: string; currentStep: number }> = ( + props +) => { + const { step, title, currentStep } = props; + return ( +
+
+
+ {step === currentStep && currentStep !== 4 && ( + + + + )} + {(currentStep > step || currentStep == 4) && ( + + + + )} + {step > currentStep && currentStep !== 4 && ( + + + + )} +
+
+
+ STEP {step} +
+
currentStep && 'text-[#64748B]')} + > + {title} +
+
+ ); +}; + +const StepSpace: FC = () => { + return ( +
+
+
+ ); +}; + +const SkipOnboarding: FC = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const onSkip = useCallback(() => { + const keys = Array.from(searchParams.keys()); + + const buildNewQuery = keys.reduce((all, current) => { + if (current === 'onboarding') { + return all; + } + const value = searchParams.get(current); + all.push(`${current}=${value}`); + return all; + }, [] as string[]).join('&'); + router.push(`?${buildNewQuery}`); + }, [searchParams]); + return ( + + ); +} +const Welcome: FC = () => { + const [step, setStep] = useState(1); + const ref = useRef(); + const router = useRouter(); + + const nextStep = useCallback(() => { + setStep(step + 1); + }, [step]); + + const firstNext = useCallback(() => { + // @ts-ignore + ref?.current?.click(); + nextStep(); + }, [nextStep]); + + const goToAnalytics = useCallback(() => { + router.push('/analytics'); + }, []); + + const goToLaunches = useCallback(() => { + router.push('/launches'); + }, []); + + return ( +
+

Onboarding

+
+ + + + + + + +
+ {step === 1 && ( + <> +
+ +
+
+ + +
+ + )} + {step === 2 && ( +
+ +
+ + +
+
+ )} + {step === 3 && ( +
+ +
+ + +
+
+ )} + {step === 4 && ( +
+
+ success +
+
+ You are done, you can now video your GitHub analytics or
schedule new posts +
+
+ + +
+
+ )} +
+ ); +}; + +export const Onboarding: FC = () => { + const query = useSearchParams(); + const modal = useModals(); + const modalOpen = useRef(false); + useEffect(() => { + const onboarding = query.get('onboarding'); + if (!onboarding) { + modalOpen.current = false; + modal.closeAll(); + return; + } + + modalOpen.current = true; + modal.openModal({ + title: '', + withCloseButton: false, + closeOnEscape: false, + classNames: { + modal: 'bg-transparent text-white', + }, + size: '100%', + children: , + }); + }, [query]); + return null; +}; diff --git a/apps/frontend/src/components/settings/github.component.tsx b/apps/frontend/src/components/settings/github.component.tsx index aa8b0cb1..75df4b78 100644 --- a/apps/frontend/src/components/settings/github.component.tsx +++ b/apps/frontend/src/components/settings/github.component.tsx @@ -43,51 +43,6 @@ const ConnectedComponent: FC<{ ); }; -const RepositoryComponent: FC<{ - id: string; - login?: string; - setRepo: (name: string) => void; -}> = (props) => { - const { setRepo, login, id } = props; - const [repositories, setRepositories] = useState< - Array<{ id: string; name: string }> - >([]); - const fetch = useFetch(); - - const loadRepositories = useCallback(async () => { - const { repositories: repolist } = await ( - await fetch(`/settings/organizations/${id}/${login}`) - ).json(); - setRepositories(repolist); - }, [login, id]); - - useEffect(() => { - setRepositories([]); - if (!login) { - return; - } - - loadRepositories(); - }, [login]); - if (!login || !repositories.length) { - return <>; - } - - return ( - - ); -}; - const ConnectComponent: FC<{ setConnected: (name: string) => void; id: string; @@ -170,13 +125,21 @@ const ConnectComponent: FC<{ export const GithubComponent: FC<{ organizations: Array<{ login: string; id: string }>; github: Array<{ id: string; login: string }>; + mutate: any; }> = (props) => { + if (typeof window !== 'undefined' && window.opener) { + window.close(); + } const { github, organizations } = props; const [githubState, setGithubState] = useState(github); + useEffect(() => { + setGithubState(github); + }, [github]); const fetch = useFetch(); + const connect = useCallback(async () => { const { url } = await (await fetch('/settings/github/url')).json(); - window.location.href = url; + window.open(url, "Github Connect", "width=700,height=700"); }, []); const setConnected = useCallback(