Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: feed settings survey #3174

Merged
merged 22 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ import ShareOptionsMenu from './ShareOptionsMenu';
import { SharedFeedPage } from './utilities';
import { FeedContainer, FeedContainerProps } from './feeds/FeedContainer';
import { ActiveFeedContext } from '../contexts';
import { useBoot, useFeedLayout, useFeedVotePost } from '../hooks';
import {
useBoot,
useConditionalFeature,
useFeedLayout,
useFeedVotePost,
useViewSize,
ViewSize,
} from '../hooks';
import {
AllFeedPages,
OtherFeedPage,
Expand All @@ -51,6 +58,7 @@ import { useFeature } from './GrowthBookProvider';
import { feature } from '../lib/featureManagement';
import { acquisitionKey } from './cards/AcquisitionFormCard';
import { MarketingCtaVariant } from './cards/MarketingCta/common';
import { useAlertsContext } from '../contexts/AlertContext';

export interface FeedProps<T>
extends Pick<
Expand Down Expand Up @@ -148,6 +156,18 @@ export default function Feed<T>({
insaneMode: listMode,
loadedSettings,
} = useContext(SettingsContext);
const { isFetched, alerts } = useAlertsContext();
const shouldEvaluateSurvey =
!!user &&
isFetched &&
alerts.shouldShowFeedFeedback &&
feedName === SharedFeedPage.MyFeed;
const { value: feedSurvey } = useConditionalFeature({
feature: feature.feedSettingsFeedback,
shouldEvaluate: shouldEvaluateSurvey,
});
const shouldShowSurvey = shouldEvaluateSurvey && feedSurvey;
const isLaptop = useViewSize(ViewSize.Laptop);
const insaneMode = !forceCardMode && listMode;
const numCards = currentSettings.numCards[spaciness ?? 'eco'];
const isSquadFeed = feedName === OtherFeedPage.Squad;
Expand Down Expand Up @@ -181,6 +201,7 @@ export default function Feed<T>({
variables,
options,
showPublicSquadsEligibility,
shouldShowSurvey: shouldShowSurvey && isLaptop,
settings: {
disableAds,
adPostLength: isSquadFeed ? 2 : undefined,
Expand Down Expand Up @@ -408,6 +429,7 @@ export default function Feed<T>({
actionButtons={actionButtons}
isHorizontal={isHorizontal}
feedContainerRef={feedContainerRef}
shouldShowSurvey={shouldShowSurvey && !isLaptop}
nensidosari marked this conversation as resolved.
Show resolved Hide resolved
>
{items.map((_, index) => (
<FeedItemComponent
Expand Down
14 changes: 14 additions & 0 deletions packages/shared/src/components/FeedItemComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { MarketingCtaCard, MarketingCtaList } from './cards';
import { MarketingCtaCardV1 } from './cards/v1/MarketingCtaCard';
import { FeedItemType } from './cards/common';
import { PublicSquadEligibilityCard } from './squads/PublicSquadEligibilityCard';
import { FeedSurveyCard } from './cards/survey';
import { FeedSettingsButton } from './feeds/FeedSettingsButton';

const CommentPopup = dynamic(
() => import(/* webpackChunkName: "commentPopup" */ './cards/CommentPopup'),
Expand Down Expand Up @@ -281,6 +283,18 @@ export default function FeedItemComponent({
showImage={!insaneMode}
/>
);
case FeedItemType.FeedSurvey:
return (
<FeedSurveyCard
title="Rate your feed quality"
max={5}
lowScore={{
value: 4,
message: 'Improve your feed by adjusting your settings.',
cta: <FeedSettingsButton className="mt-4" />,
}}
/>
);
case FeedItemType.UserAcquisition:
return <AcquisitionFormCard key="user-acquisition-card" />;
case FeedItemType.PublicSquadEligibility:
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/components/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ function ButtonComponent<TagName extends AllowedTags>(
aria-pressed={pressed}
ref={ref}
className={classNames(
`btn shadow-none focus-outline inline-flex cursor-pointer select-none
flex-row items-center border no-underline transition
`btn focus-outline inline-flex cursor-pointer select-none flex-row
items-center border no-underline shadow-none transition
duration-200 ease-in-out typo-callout`,
variant !== ButtonVariant.Option && 'justify-center font-bold',
{ iconOnly },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function MarketingCtaCard({

trackEvent({
event_name: AnalyticsEvent.Impression,
target_type: TargetType.MarketingCtaCard,
target_type: TargetType.PromotionCard,
target_id: marketingCta.campaignId,
});
isImpressionTracked.current = true;
Expand All @@ -33,7 +33,7 @@ export function MarketingCtaCard({
const onCtaClick = useCallback(() => {
trackEvent({
event_name: AnalyticsEvent.Click,
target_type: TargetType.MarketingCtaCard,
target_type: TargetType.PromotionCard,
target_id: marketingCta.campaignId,
});
clearMarketingCta(marketingCta.campaignId);
Expand All @@ -42,7 +42,7 @@ export function MarketingCtaCard({
const onCtaDismiss = useCallback(() => {
trackEvent({
event_name: AnalyticsEvent.MarketingCtaDismiss,
target_type: TargetType.MarketingCtaCard,
target_type: TargetType.PromotionCard,
target_id: marketingCta.campaignId,
});
clearMarketingCta(marketingCta.campaignId);
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/cards/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,5 @@ export enum FeedItemType {
UserAcquisition = 'userAcquisition',
PublicSquadEligibility = 'publicSquadEligibility',
MarketingCta = 'marketingCta',
FeedSurvey = 'feedSurvey',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { ReactElement, useState } from 'react';
import { RatingStars } from '../../utilities/RatingStars';
import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button';
import { useFeedSurvey } from '../../../hooks/feed/useFeedSurvey';
import { FeedSurveyProps } from './common';
import { feedSurveyBg, feedSurveyTopBorder } from '../../../styles/custom';

export function FeedSurveyBanner({
max,
title,
lowScore,
postFeedbackMessage = 'Thank you for your feedback!',
}: FeedSurveyProps): ReactElement {
const [score, setScore] = useState(0);
const { submitted, onSubmit, onHide } = useFeedSurvey({ score });

return (
<div className="mb-4 pt-0.5" style={{ background: feedSurveyTopBorder }}>
<div
className="flex flex-col px-4 py-6 tablet:px-6"
style={{ background: feedSurveyBg }}
>
<div className="flex flex-row items-start tablet:items-center">
<div className="flex flex-1 flex-col items-start justify-between tablet:flex-row tablet:items-center">
<h3 className="font-bold typo-title3">{title}</h3>
<RatingStars max={max} onStarClick={setScore} />
</div>
<Button
variant={score > 0 ? ButtonVariant.Primary : ButtonVariant.Float}
size={ButtonSize.Small}
onClick={score > 0 ? onSubmit : onHide}
className="ml-2 w-24"
disabled={submitted}
>
{score > 0 ? 'Submit' : 'Hide'}
</Button>
</div>
{submitted && (
<div className="mt-6 flex flex-col tablet:flex-row">
<p className="shrink text-text-secondary typo-body">
<strong className="mr-1">{postFeedbackMessage}</strong>
{score <= lowScore.value ? lowScore.message : null}
</p>
{score <= lowScore.value && lowScore.cta}
</div>
)}
</div>
</div>
);
}
66 changes: 66 additions & 0 deletions packages/shared/src/components/cards/survey/FeedSurveyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { ReactElement, useState } from 'react';
import classNames from 'classnames';
import { Card as CardV1 } from '../v1/Card';
import { Card } from '../Card';
import { useFeedLayout } from '../../../hooks';
import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button';
import { RatingStars } from '../../utilities/RatingStars';
import { useFeedSurvey } from '../../../hooks/feed/useFeedSurvey';
import { FeedSurveyProps } from './common';
import { feedSurveyBg, feedSurveyBorder } from '../../../styles/custom';

export function FeedSurveyCard({
nensidosari marked this conversation as resolved.
Show resolved Hide resolved
max,
title,
lowScore,
postFeedbackMessage = 'Thank you for your feedback!',
}: FeedSurveyProps): ReactElement {
const [score, setScore] = useState(0);
const { submitted, onSubmit, onHide } = useFeedSurvey({ score });
const { shouldUseListFeedLayout } = useFeedLayout();
const CardComponent = shouldUseListFeedLayout ? CardV1 : Card;

return (
<div
className="relative rounded-16 p-0.5"
style={{ backgroundImage: feedSurveyBorder }}
>
<CardComponent
className={classNames(
'relative items-center gap-4 overflow-hidden !rounded-14 border-0 shadow-none',
shouldUseListFeedLayout ? '!py-10' : 'pt-20',
)}
style={{ background: feedSurveyBg }}
>
<h3 className="font-bold typo-title3">{title}</h3>
<RatingStars max={max} isDisabled={submitted} onStarClick={setScore} />
{submitted && (
<p className="mt-2 text-center text-text-secondary typo-body">
<span className="font-bold">{postFeedbackMessage}</span>
{score <= lowScore.value && <p>{lowScore.message}</p>}
{score <= lowScore.value && lowScore.cta}
</p>
)}
{score > 0 && !submitted && (
<Button
variant={ButtonVariant.Primary}
size={ButtonSize.Small}
onClick={onSubmit}
>
Submit
</Button>
)}
{score === 0 && (
<Button
variant={ButtonVariant.Float}
size={ButtonSize.Small}
className="mb-2 mt-auto"
onClick={onHide}
>
Hide
</Button>
)}
</CardComponent>
</div>
);
}
12 changes: 12 additions & 0 deletions packages/shared/src/components/cards/survey/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactNode } from 'react';

export interface FeedSurveyProps {
title: string;
postFeedbackMessage?: string;
max: number;
lowScore: {
value: number;
message: string;
cta: ReactNode;
};
}
2 changes: 2 additions & 0 deletions packages/shared/src/components/cards/survey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './FeedSurveyBanner';
export * from './FeedSurveyCard';
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function MarketingCtaCardV1({

trackEvent({
event_name: AnalyticsEvent.Impression,
target_type: TargetType.MarketingCtaCard,
target_type: TargetType.PromotionCard,
target_id: marketingCta.campaignId,
});
isImpressionTracked.current = true;
Expand All @@ -39,7 +39,7 @@ export function MarketingCtaCardV1({
const onCtaClick = useCallback(() => {
trackEvent({
event_name: AnalyticsEvent.Click,
target_type: TargetType.MarketingCtaCard,
target_type: TargetType.PromotionCard,
target_id: marketingCta.campaignId,
});
clearMarketingCta(marketingCta.campaignId);
Expand All @@ -48,7 +48,7 @@ export function MarketingCtaCardV1({
const onCtaDismiss = useCallback(() => {
trackEvent({
event_name: AnalyticsEvent.MarketingCtaDismiss,
target_type: TargetType.MarketingCtaCard,
target_type: TargetType.PromotionCard,
target_id: marketingCta.campaignId,
});
clearMarketingCta(marketingCta.campaignId);
Expand Down
15 changes: 15 additions & 0 deletions packages/shared/src/components/feeds/FeedContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import ConditionalWrapper from '../ConditionalWrapper';
import { useActiveFeedNameContext } from '../../contexts';
import { feature } from '../../lib/featureManagement';
import { SharedFeedPage } from '../utilities';
import { FeedSurveyBanner } from '../cards/survey';
import { FeedSettingsButton } from './FeedSettingsButton';

export interface FeedContainerProps {
children: ReactNode;
Expand All @@ -37,6 +39,7 @@ export interface FeedContainerProps {
shortcuts?: ReactNode;
actionButtons?: ReactNode;
isHorizontal?: boolean;
shouldShowSurvey?: boolean;
feedContainerRef?: React.Ref<HTMLDivElement>;
}

Expand Down Expand Up @@ -145,6 +148,7 @@ export const FeedContainer = ({
shortcuts,
actionButtons,
isHorizontal,
shouldShowSurvey,
feedContainerRef,
}: FeedContainerProps): ReactElement => {
const { value: isShortcutsV1 } = useConditionalFeature({
Expand Down Expand Up @@ -269,6 +273,17 @@ export const FeedContainer = ({
</div>
)}
>
{shouldShowSurvey && (
<FeedSurveyBanner
title="Rate your feed quality"
max={5}
lowScore={{
value: 4,
message: 'Improve your feed by adjusting your settings.',
cta: <FeedSettingsButton className="mt-4 w-fit" />,
}}
Comment on lines +279 to +284
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these props actually? We always pass them exactly the same?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized we may have another survey that could use the same component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, just felt a bit overkill seeing we use it in 2 spots

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, kinda having that dilemma too. I can maybe store these common props somewhere to make them consistent.

/>
)}
<div
className={classNames(
'grid',
Expand Down
42 changes: 42 additions & 0 deletions packages/shared/src/components/feeds/FeedSettingsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { ReactElement } from 'react';
import {
Button,
ButtonProps,
ButtonSize,
ButtonVariant,
} from '../buttons/Button';
import { useAnalyticsContext } from '../../contexts/AnalyticsContext';
import { AnalyticsEvent } from '../../lib/analytics';
import { LazyModal } from '../modals/common/types';
import { useLazyModal } from '../../hooks/useLazyModal';
import { FilterIcon } from '../icons';

export function FeedSettingsButton({
onClick,
children = 'Feed settings',
...props
}: ButtonProps<'button'>): ReactElement {
const { trackEvent } = useAnalyticsContext();
const { openModal } = useLazyModal();
const onButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
trackEvent({ event_name: AnalyticsEvent.ManageTags });

if (onClick) {
onClick(event);
} else {
openModal({ type: LazyModal.FeedFilters, persistOnRouteChange: true });
}
};

return (
<Button
variant={ButtonVariant.Primary}
size={ButtonSize.Small}
icon={<FilterIcon />}
{...props}
onClick={onButtonClick}
>
{children}
</Button>
);
}
Loading