feat: featurebase integration (#2425)

* feat(utils): add featurebase utils

* feat(config): add changelog, feedback and roadmap routes

* feat(app): add featurebase script

* feat(docs): add NEXT_PUBLIC_FB_FEEDBACK_URL

* feat(featurebase): add featurebase components

* feat(components): add featurebase components to navbar

* feat(components): add featurebase components to sidebar

* chore(config): remove changelog and feedback at this moment

* fix(components): fb-roadmap-link styles

* chore(components): hide feedback and changelog at this moment

* feat(docs): add NEXT_PUBLIC_FB_FEEDBACK_ORG

* feat(featurebase): add trackEvent & revise props
This commit is contained in:
աӄա 2024-03-04 03:20:40 +08:00 committed by GitHub
parent fa27bccb37
commit 4957f56e86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 256 additions and 61 deletions

View File

@ -12,4 +12,8 @@ IS_PREVIEW=true/false
ANALYZE_BUNDLE=true/false
# Vercel preview env (is used for taking the docs directly from the project files)
NEXT_PUBLIC_PREVIEW=true/false
NEXT_PUBLIC_PREVIEW=true/false
## Featurebase
NEXT_PUBLIC_FB_FEEDBACK_ORG=
NEXT_PUBLIC_FB_FEEDBACK_URL=

View File

@ -1,6 +1,7 @@
import "@/styles/globals.css";
import "@/styles/sandpack.css";
import {Metadata} from "next";
import Script from "next/script";
import {clsx} from "@nextui-org/shared-utils";
import {Analytics} from "@vercel/analytics/react";
@ -79,6 +80,7 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
<Cmdk />
</Providers>
{__PROD__ && <Analytics />}
<Script id="featurebase-sdk" src="https://do.featurebase.app/js/sdk.js" />
</body>
</html>
);

View File

@ -28,6 +28,10 @@ import {getRoutePaths} from "./utils";
import {Route} from "@/libs/docs/page";
import {TreeKeyboardDelegate} from "@/utils/tree-keyboard-delegate";
import {trackEvent} from "@/utils/va";
import {FbFeedbackButton} from "@/components/featurebase/fb-feedback-button";
import {FbChangelogButton} from "@/components/featurebase/fb-changelog-button";
import {FbRoadmapLink} from "@/components/featurebase/fb-roadmap-link";
import {openFeedbackWidget} from "@/utils/featurebase";
export interface Props<T> extends Omit<ItemProps<T>, "title">, Route {
slug?: string;
@ -85,6 +89,20 @@ function TreeItem<T>(props: TreeItemProps<T>) {
const Component = hasChildNodes ? "ul" : "li";
const cn = clsx(
"w-full",
"font-normal",
"before:mr-4",
"before:content-['']",
"before:block",
"before:bg-default-300",
"before:w-1",
"before:h-1",
"before:rounded-full",
"opacity-80",
"dark:opacity-60",
);
const {pressProps} = usePress({
onPress: () => {
if (hasChildNodes) {
@ -103,6 +121,79 @@ function TreeItem<T>(props: TreeItemProps<T>) {
const {focusProps, isFocused, isFocusVisible} = useFocusRing();
const renderFeaturebaseComponent = (key: string) => {
if (key === "roadmap")
return <FbRoadmapLink className={cn} innerClassName="opacity-80 dark:opacity-60" />;
if (key === "changelog")
return (
<NextUILink as={Link} className={cn} color="foreground" href="#">
<FbChangelogButton />
</NextUILink>
);
return (
<NextUILink as={Link} className={cn} color="foreground" href="#" onClick={openFeedbackWidget}>
<FbFeedbackButton />
</NextUILink>
);
};
const renderComponent = () => {
if (hasChildNodes) {
return (
<span className="flex items-center gap-3">
<span>{rendered}</span>
<ChevronIcon
className={clsx("transition-transform", {
"-rotate-90": isExpanded,
})}
/>
</span>
);
}
if (typeof key === "string" && ["changelog", "feedback", "roadmap"].includes(key)) {
return renderFeaturebaseComponent(key);
}
return (
<NextUILink as={Link} className={clsx(cn)} color="foreground" href={paths.pathname}>
<span
className={clsx(
isSelected
? "text-primary font-medium dark:text-foreground"
: "opacity-80 dark:opacity-60",
{
"pointer-events-none": item.props?.comingSoon,
},
)}
>
{rendered}
</span>
{isUpdated && (
<Chip
className="ml-1 py-1 text-tiny text-default-400 bg-default-100/50"
color="default"
size="sm"
variant="flat"
>
Updated
</Chip>
)}
{isNew && (
<Chip className="ml-1 py-1 text-tiny" color="primary" size="sm" variant="flat">
New
</Chip>
)}
{item.props?.comingSoon && (
<Chip className="ml-1 py-1 text-tiny" color="default" size="sm" variant="flat">
Coming soon
</Chip>
)}
</NextUILink>
);
};
return (
<Component
{...focusProps}
@ -122,66 +213,7 @@ function TreeItem<T>(props: TreeItemProps<T>) {
>
<div className="flex items-center gap-3 cursor-pointer" {...pressProps}>
<Spacer x={spaceLeft} />
{hasChildNodes ? (
<span className="flex items-center gap-3">
<span>{rendered}</span>
<ChevronIcon
className={clsx("transition-transform", {
"-rotate-90": isExpanded,
})}
/>
</span>
) : (
<NextUILink
as={Link}
className={clsx(
"w-full",
"font-normal",
"before:mr-4",
"before:content-['']",
"before:block",
"before:bg-default-300",
"before:w-1",
"before:h-1",
"before:rounded-full",
)}
color="foreground"
href={paths.pathname}
>
<span
className={clsx(
isSelected
? "text-primary font-medium dark:text-foreground"
: "opacity-80 dark:opacity-60",
{
"pointer-events-none": item.props?.comingSoon,
},
)}
>
{rendered}
</span>
{isUpdated && (
<Chip
className="ml-1 py-1 text-tiny text-default-400 bg-default-100/50"
color="default"
size="sm"
variant="flat"
>
Updated
</Chip>
)}
{isNew && (
<Chip className="ml-1 py-1 text-tiny" color="primary" size="sm" variant="flat">
New
</Chip>
)}
{item.props?.comingSoon && (
<Chip className="ml-1 py-1 text-tiny" color="default" size="sm" variant="flat">
Coming soon
</Chip>
)}
</NextUILink>
)}
{renderComponent()}
{/* Workaround to avoid scrollbar overlapping */}
<Spacer x={4} />
</div>

View File

@ -0,0 +1,46 @@
"use client";
import {useEffect} from "react";
import {trackEvent} from "@/utils/va";
type Props = {
className?: string;
};
// ref: https://developers.featurebase.app/install/changelog-widget/install
export const FbChangelogButton = ({className}: Props) => {
useEffect(() => {
const win = window as any;
if (typeof win.Featurebase !== "function") {
win.Featurebase = function () {
// eslint-disable-next-line prefer-rest-params
(win.Featurebase.q = win.Featurebase.q || []).push(arguments);
};
}
win.Featurebase("initialize_changelog_widget", {
organization: process.env.NEXT_PUBLIC_FB_FEEDBACK_ORG,
theme: "dark",
usersName: "",
fullscreenPopup: true,
alwaysShow: true,
});
}, []);
const fbButtonOnClick = () => {
(window as any).Featurebase("manually_open_changelog_popup");
trackEvent("Featurebase - Changelog", {
name: "featurebase-changelog",
action: "press",
category: "featurebase",
});
};
return (
<button className={className} onClick={fbButtonOnClick}>
Changelog <span id="fb-update-badge" />
</button>
);
};

View File

@ -0,0 +1,42 @@
"use client";
import {useEffect} from "react";
import {trackEvent} from "@/utils/va";
type Props = {
className?: string;
};
// ref: https://developers.featurebase.app/install/feedback-widget/setup
export const FbFeedbackButton = ({className}: Props) => {
useEffect(() => {
const win = window as any;
if (typeof win.Featurebase !== "function") {
win.Featurebase = function () {
// eslint-disable-next-line prefer-rest-params
(win.Featurebase.q = win.Featurebase.q || []).push(arguments);
};
}
win.Featurebase("initialize_feedback_widget", {
organization: process.env.NEXT_PUBLIC_FB_FEEDBACK_ORG,
theme: "dark",
email: "",
});
}, []);
const fbButtonOnClick = () => {
trackEvent("Featurebase - Feedback", {
name: "featurebase-feedback",
action: "press",
category: "featurebase",
});
};
return (
<button data-featurebase-feedback className={className} onClick={fbButtonOnClick}>
Feedback
</button>
);
};

View File

@ -0,0 +1,42 @@
"use client";
import NextLink from "next/link";
import arrowRightUpIcon from "@iconify/icons-solar/arrow-right-up-linear";
import {Icon} from "@iconify/react/dist/offline";
import {clsx} from "@nextui-org/shared-utils";
import {trackEvent} from "@/utils/va";
type Props = {
className?: string;
innerClassName?: string;
};
export const FbRoadmapLink = ({className, innerClassName}: Props) => {
const fbLinkOnClick = () => {
trackEvent("Featurebase - Roadmap", {
name: "featurebase-roadmap",
action: "press",
category: "featurebase",
});
};
return (
<NextLink
className={clsx("inline-flex items-center", className)}
color="foreground"
href={`${process.env.NEXT_PUBLIC_FB_FEEDBACK_URL}/roadmap`}
target="_blank"
onClick={fbLinkOnClick}
>
<div className={clsx("relative", innerClassName)}>
Roadmap
<Icon
className="absolute right-[-10px] top-0 outline-none transition-transform group-data-[hover=true]:translate-y-0.5 [&>path]:stroke-[2.5px]"
icon={arrowRightUpIcon}
width={10}
/>
</div>
</NextLink>
);
};

View File

@ -43,6 +43,7 @@ import {
import {useIsMounted} from "@/hooks/use-is-mounted";
import {DocsSidebar} from "@/components/docs/sidebar";
import {useCmdkStore} from "@/components/cmdk";
import {FbRoadmapLink} from "@/components/featurebase/fb-roadmap-link";
import {trackEvent} from "@/utils/va";
export interface NavbarProps {
@ -241,6 +242,20 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
Figma
</NextLink>
</NavbarItem>
{/* hide feedback and changelog at this moment */}
{/* <NavbarItem>
<NextLink className={navLinkClasses} color="foreground" href="#">
<FbChangelogButton key="changelog" userName="" />
</NextLink>
</NavbarItem>
<NavbarItem>
<NextLink className={navLinkClasses} color="foreground" href="#">
<FbFeedbackButton key="feedback" userEmail="" />
</NextLink>
</NavbarItem> */}
<NavbarItem>
<FbRoadmapLink className={navLinkClasses} />
</NavbarItem>
{/* <NavbarItem>
<Chip
as={NextLink}

View File

@ -394,6 +394,12 @@
"title": "Figma",
"keywords": "figma, nextui, design, ui kit",
"path": "/figma"
},
{
"key": "roadmap",
"title": "Roadmap",
"keywords": "roadmap, nextui",
"path": "#"
}
]
}

View File

@ -0,0 +1,6 @@
export const openFeedbackWidget = () => {
window.postMessage({
target: "FeaturebaseWidget",
data: {action: "openFeedbackWidget"},
});
};