Skip to content

Commit

Permalink
feat(review): Satisfaction modal for advanced repository
Browse files Browse the repository at this point in the history
  • Loading branch information
xrutayisire committed Oct 12, 2023
1 parent f3f7a5d commit b53b2f6
Show file tree
Hide file tree
Showing 15 changed files with 341 additions and 120 deletions.
1 change: 1 addition & 0 deletions packages/manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
PrismicRepository,
FrameworkWroomTelemetryID,
StarterId,
DocumentStatus,
} from "./managers/prismicRepository/types";

export type { SliceMachineManager } from "./managers/SliceMachineManager";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@ import {
TransactionalMergeReturnType,
FrameworkWroomTelemetryID,
StarterId,
DocumentStatus,
} from "./types";
import { assertPluginsInitialized } from "../../lib/assertPluginsInitialized";
import { UnauthenticatedError } from "../../errors";
import { UnauthenticatedError, UnexpectedDataError } from "../../errors";

const DEFAULT_REPOSITORY_SETTINGS = {
plan: "personal",
isAnnual: "false",
role: "developer",
};

const PrismicDocumentsCount = t.exact(
t.type({
count: t.number,
}),
);

type PrismicDocumentsCount = t.TypeOf<typeof PrismicDocumentsCount>;

type PrismicRepositoryManagerCheckExistsArgs = {
domain: string;
};
Expand All @@ -57,6 +66,14 @@ type PrismicRepositoryManagerPushDocumentsArgs = {
documents: Record<string, unknown>; // TODO: Type unknown if possible(?)
};

type PrismicRepositoryManagerGetDocumentsCountArgs = {
statuses: DocumentStatus[];
};

type PrismicRepositoryManagerGetDocumentsCountReturnType = {
count: number;
};

export class PrismicRepositoryManager extends BaseManager {
// TODO: Add methods for repository-specific actions. E.g. creating a
// new repository.
Expand Down Expand Up @@ -258,6 +275,54 @@ export class PrismicRepositoryManager extends BaseManager {
}
}

async getDocumentsCount(
args: PrismicRepositoryManagerGetDocumentsCountArgs,
): Promise<PrismicRepositoryManagerGetDocumentsCountReturnType> {
const { statuses } = args;
const statusParams = statuses.map((status) => `status=${status}`).join("&");
const url = new URL(
`./core/documents/count?${statusParams}`,
API_ENDPOINTS.PrismicWroom,
);

const repositoryName = await this.project.getRepositoryName();
// Update hostname to include repository domain
url.hostname = `${repositoryName}.${url.hostname}`;

const res = await this._fetch({
url,
method: "GET",
userAgent: PrismicRepositoryUserAgent.LegacyZero, // Custom User Agent is required,
});

if (res.ok) {
const json = await res.json();
const { value, error } = decode(PrismicDocumentsCount, json);

if (error || !value) {
throw new UnexpectedDataError(
`Received invalid data while getting documents count for repository "${repositoryName}."`,
);
}

return value;
}

let reason: string | null = null;
try {
reason = await res.text();
} catch {
// Noop
}

throw new Error(
`Failed to fetch documents count for repository "${repositoryName}.", ${res.status} ${res.statusText}`,
{
cause: reason,
},
);
}

async pushChanges(
args: TransactionalMergeArgs,
): Promise<TransactionalMergeReturnType> {
Expand Down
2 changes: 2 additions & 0 deletions packages/manager/src/managers/prismicRepository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,5 @@ export type StarterId =
| "nuxt_multi_page"
| "nuxt_blog"
| "nuxt_multi_lang";

export type DocumentStatus = "published" | "draft";
6 changes: 5 additions & 1 deletion packages/manager/src/managers/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ type CommandInitEndSegmentEvent = SegmentEvent<

type ReviewSegmentEvent = SegmentEvent<
typeof SegmentEventType.review,
{ rating: number; comment: string }
{
rating: number;
comment: string;
type: "onboarding" | "advanced repository";
}
>;

type SliceSimulatorSetupSegmentEvent = SegmentEvent<
Expand Down
2 changes: 1 addition & 1 deletion packages/slice-machine/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BaseStyles } from "theme-ui";

import { AppLayout, AppLayoutContent } from "@components/AppLayout";
import LoginModal from "@components/LoginModal";
import ReviewModal from "@components/ReviewModal";
import { ReviewModal } from "@components/ReviewModal/ReviewModal";
import { MissingLibraries } from "@components/MissingLibraries";
import useServerState from "@src/hooks/useServerState";
import { SliceMachineStoreType } from "@src/redux/type";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FC } from "react";

import { useDocumentsCount } from "@src/hooks/useDocumentsCount";

import { ReviewForm } from "./ReviewForm";

export const AdvancedRepositoryReviewModal: FC = () => {
const documentsCount = useDocumentsCount(["published"]);
const isAdvancedRepository =
documentsCount !== undefined && documentsCount >= 20;

return isAdvancedRepository ? (
<ReviewForm reviewType="advancedRepository" />
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FC } from "react";
import { useSelector } from "react-redux";

import { SliceMachineStoreType } from "@src/redux/type";
import { getLastSyncChange } from "@src/modules/userContext";
import { selectAllCustomTypes } from "@src/modules/availableCustomTypes";
import { getLibraries } from "@src/modules/slices";
import { hasLocal } from "@lib/models/common/ModelData";

import { ReviewForm } from "./ReviewForm";

export const OnboardingReviewModal: FC = () => {
const { customTypes, libraries, lastSyncChange } = useSelector(
(store: SliceMachineStoreType) => ({
customTypes: selectAllCustomTypes(store),
libraries: getLibraries(store),
lastSyncChange: getLastSyncChange(store),
})
);

const sliceCount =
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
libraries && libraries.length
? libraries.reduce((count, lib) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!lib) return count;
return count + lib.components.length;
}, 0)
: 0;

const hasSliceWithinCustomType = customTypes.some(
(customType) =>
hasLocal(customType) &&
customType.local.tabs.some(
(tab) => tab.sliceZone && tab.sliceZone?.value.length > 0
)
);

const hasPushedAnHourAgo = Boolean(
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
lastSyncChange && Date.now() - lastSyncChange >= 3600000
);

const isOnboardingDone =
sliceCount >= 1 &&
customTypes.length >= 1 &&
hasSliceWithinCustomType &&
hasPushedAnHourAgo;

return isOnboardingDone ? <ReviewForm reviewType="onboarding" /> : null;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Modal from "react-modal";
import SliceMachineModal from "@components/SliceMachineModal";
import { Field, FieldProps, Form, Formik } from "formik";
import { FC } from "react";
import { Field, Form, Formik } from "formik";
import {
Box,
Button,
Expand All @@ -11,107 +10,50 @@ import {
Text,
Textarea,
} from "theme-ui";
import Modal from "react-modal";
import { useSelector } from "react-redux";

import SliceMachineModal from "@components/SliceMachineModal";
import { SliceMachineStoreType } from "@src/redux/type";
import { isModalOpen } from "@src/modules/modal";
import { isLoading } from "@src/modules/loading";
import { LoadingKeysEnum } from "@src/modules/loading/types";
import {
getLastSyncChange,
userHasSendAReview,
} from "@src/modules/userContext";
import useSliceMachineActions from "@src/modules/useSliceMachineActions";
import { ModalKeysEnum } from "@src/modules/modal/types";
import { telemetry } from "@src/apiClient";
import { selectAllCustomTypes } from "@src/modules/availableCustomTypes";
import { getLibraries } from "@src/modules/slices";
import { hasLocal } from "@lib/models/common/ModelData";
import { UserReview } from "@src/modules/userContext/types";

Modal.setAppElement("#__next");
import { ReviewFormSelect } from "./ReviewFormSelect";

const ratingSelectable = [1, 2, 3, 4, 5];
Modal.setAppElement("#__next");

const SelectReviewComponent = ({ field, form }: FieldProps) => {
return (
<Box sx={{ mb: 3, display: "flex", justifyContent: "space-between" }}>
{ratingSelectable.map((rating, index) => (
<Button
variant="secondary"
type="button"
key={index}
onClick={() => void form.setFieldValue("rating", rating)}
className={field.value === rating ? "selected" : ""}
sx={{
"&:not(:last-of-type)": {
mr: 1,
},
"&.selected": {
backgroundColor: "code.gray",
color: "white",
},
}}
data-cy={`review-form-score-${rating}`}
>
{rating}
</Button>
))}
</Box>
);
type ReviewFormProps = {
reviewType: keyof UserReview;
};

const ReviewModal: React.FunctionComponent = () => {
const {
isReviewLoading,
isLoginModalOpen,
hasSendAReview,
customTypes,
libraries,
lastSyncChange,
} = useSelector((store: SliceMachineStoreType) => ({
isReviewLoading: isLoading(store, LoadingKeysEnum.REVIEW),
isLoginModalOpen: isModalOpen(store, ModalKeysEnum.LOGIN),
hasSendAReview: userHasSendAReview(store),
customTypes: selectAllCustomTypes(store),
libraries: getLibraries(store),
lastSyncChange: getLastSyncChange(store),
}));

export const ReviewForm: FC<ReviewFormProps> = (props) => {
const { reviewType } = props;
const { isReviewLoading, isLoginModalOpen } = useSelector(
(store: SliceMachineStoreType) => ({
isReviewLoading: isLoading(store, LoadingKeysEnum.REVIEW),
isLoginModalOpen: isModalOpen(store, ModalKeysEnum.LOGIN),
})
);
const { skipReview, sendAReview, startLoadingReview, stopLoadingReview } =
useSliceMachineActions();

const sliceCount =
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
libraries && libraries.length
? libraries.reduce((count, lib) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!lib) return count;
return count + lib.components.length;
}, 0)
: 0;

const hasSliceWithinCustomType = customTypes.some(
(customType) =>
hasLocal(customType) &&
customType.local.tabs.some(
(tab) => tab.sliceZone && tab.sliceZone?.value.length > 0
)
);

const hasPushedAnHourAgo = Boolean(
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
lastSyncChange && Date.now() - lastSyncChange >= 3600000
);

const userHasCreatedEnoughContent =
sliceCount >= 1 &&
customTypes.length >= 1 &&
hasSliceWithinCustomType &&
hasPushedAnHourAgo;

const onSendAReview = (rating: number, comment: string): void => {
startLoadingReview();
void telemetry.track({ event: "review", rating, comment });
sendAReview();
void telemetry.track({
event: "review",
rating,
comment,
type:
reviewType === "advancedRepository"
? "advanced repository"
: "onboarding",
});
sendAReview(reviewType);
stopLoadingReview();
};

Expand All @@ -123,9 +65,9 @@ const ReviewModal: React.FunctionComponent = () => {

return (
<SliceMachineModal
isOpen={userHasCreatedEnoughContent && !hasSendAReview}
isOpen
shouldCloseOnOverlayClick={false}
onRequestClose={() => skipReview()}
onRequestClose={() => skipReview(reviewType)}
closeTimeoutMS={500}
contentLabel={"Review Modal"}
portalClassName={"ReviewModal"}
Expand Down Expand Up @@ -178,9 +120,9 @@ const ReviewModal: React.FunctionComponent = () => {
}}
>
<Heading sx={{ fontSize: "20px", mr: 4 }}>
Share Feedback
Share feedback
</Heading>
<Close type="button" onClick={() => skipReview()} />
<Close type="button" onClick={() => skipReview(reviewType)} />
</Flex>
<Flex
sx={{
Expand All @@ -190,8 +132,8 @@ const ReviewModal: React.FunctionComponent = () => {
}}
>
<Text variant={"xs"} as={"p"} sx={{ maxWidth: 302, mb: 3 }}>
Overall, how satisfied are you with your Slice Machine
experience?
Overall, how satisfied or dissatisfied are you with your Slice
Machine experience so far?
</Text>
<Box
mb={2}
Expand All @@ -208,11 +150,11 @@ const ReviewModal: React.FunctionComponent = () => {
Very satisfied
</Text>
</Box>
<Field name={"rating"} component={SelectReviewComponent} />
<Field name={"rating"} component={ReviewFormSelect} />
<Field
name={"comment"}
type="text"
placeholder="Share your thoughts. What can we improve?"
placeholder="Tell me more..."
as={Textarea}
autoComplete="off"
sx={{ height: 80, mb: 3 }}
Expand All @@ -233,5 +175,3 @@ const ReviewModal: React.FunctionComponent = () => {
</SliceMachineModal>
);
};

export default ReviewModal;
Loading

0 comments on commit b53b2f6

Please sign in to comment.