From 015d4c7663dfdf8e18174218285cffd2da388a5c Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:27:28 +0530 Subject: [PATCH] chore: Simpler Sharing Page [Concept] (#2361) Co-authored-by: pandeymangg --- .../components/SurveyResultsTabs.tsx | 11 +- .../responses/components/ResponsePage.tsx | 55 +++- .../responses/components/ResponseTimeline.tsx | 21 +- .../summary/components/SummaryPage.tsx | 34 ++- .../[surveyId]/components/CustomFilter.tsx | 103 +++---- .../components/ResultsShareButton.tsx | 4 +- .../[surveyId]/components/SummaryHeader.tsx | 265 +++++++++--------- .../components/SurveyResultsTabs.tsx | 63 ----- .../(analysis)/responses/actions.ts | 40 --- .../responses/components/ResponsePage.tsx | 144 ---------- .../responses/components/ResponseTimeline.tsx | 103 ------- .../(analysis)/responses/page.tsx | 7 +- .../summary/components/SummaryPage.tsx | 138 --------- .../[sharingKey]/(analysis)/summary/page.tsx | 8 +- .../[sharingKey]/components/CustomFilter.tsx | 241 ---------------- .../[sharingKey]/components/SummaryHeader.tsx | 21 -- 16 files changed, 298 insertions(+), 960 deletions(-) delete mode 100644 apps/web/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs.tsx delete mode 100644 apps/web/app/share/[sharingKey]/(analysis)/responses/actions.ts delete mode 100644 apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx delete mode 100644 apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx delete mode 100644 apps/web/app/share/[sharingKey]/(analysis)/summary/components/SummaryPage.tsx delete mode 100755 apps/web/app/share/[sharingKey]/components/CustomFilter.tsx delete mode 100644 apps/web/app/share/[sharingKey]/components/SummaryHeader.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs.tsx index 392bfb7ecb..834eaf5e90 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs.tsx @@ -1,6 +1,7 @@ import revalidateSurveyIdPath from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"; import { InboxIcon, PresentationIcon } from "lucide-react"; import Link from "next/link"; +import { useParams } from "next/navigation"; import { cn } from "@formbricks/lib/cn"; @@ -17,18 +18,24 @@ export default function SurveyResultsTab({ surveyId, responseCount, }: SurveyResultsTabProps) { + const params = useParams(); + const sharingKey = params.sharingKey as string; + const isSharingPage = !!sharingKey; + + const url = isSharingPage ? `/share/${sharingKey}` : `/environments/${environmentId}/surveys/${surveyId}`; + const tabs = [ { id: "summary", label: "Summary", icon: , - href: `/environments/${environmentId}/surveys/${surveyId}/summary?referer=true`, + href: `${url}/summary?referer=true`, }, { id: "responses", label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`, icon: , - href: `/environments/${environmentId}/surveys/${surveyId}/responses?referer=true`, + href: `${url}/responses?referer=true`, }, ]; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx index debad97acd..a820fa3712 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx @@ -10,7 +10,11 @@ import ResponseTimeline from "@/app/(app)/environments/[environmentId]/surveys/[ import CustomFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter"; import SummaryHeader from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SummaryHeader"; import { getFormattedFilters } from "@/app/lib/surveys/surveys"; -import { useSearchParams } from "next/navigation"; +import { + getResponseCountBySurveySharingKeyAction, + getResponsesBySurveySharingKeyAction, +} from "@/app/share/[sharingKey]/action"; +import { useParams, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall"; @@ -31,7 +35,7 @@ interface ResponsePageProps { surveyId: string; webAppUrl: string; product: TProduct; - user: TUser; + user?: TUser; environmentTags: TTag[]; attributes: TSurveyPersonAttributes; responsesPerPage: number; @@ -52,6 +56,10 @@ const ResponsePage = ({ membershipRole, totalResponseCount, }: ResponsePageProps) => { + const params = useParams(); + const sharingKey = params.sharingKey as string; + const isSharingPage = !!sharingKey; + const [responseCount, setResponseCount] = useState(null); const [responses, setResponses] = useState([]); const [page, setPage] = useState(1); @@ -73,13 +81,26 @@ const ResponsePage = ({ const fetchNextPage = useCallback(async () => { const newPage = page + 1; - const newResponses = await getResponsesAction(surveyId, newPage, responsesPerPage, filters); + + let newResponses: TResponse[] = []; + + if (isSharingPage) { + newResponses = await getResponsesBySurveySharingKeyAction( + sharingKey, + newPage, + responsesPerPage, + filters + ); + } else { + newResponses = await getResponsesAction(surveyId, newPage, responsesPerPage, filters); + } + if (newResponses.length === 0 || newResponses.length < responsesPerPage) { setHasMore(false); } setResponses([...responses, ...newResponses]); setPage(newPage); - }, [filters, page, responses, responsesPerPage, surveyId]); + }, [filters, isSharingPage, page, responses, responsesPerPage, sharingKey, surveyId]); const deleteResponse = (responseId: string) => { setResponses(responses.filter((response) => response.id !== responseId)); @@ -100,17 +121,32 @@ const ResponsePage = ({ useEffect(() => { const handleResponsesCount = async () => { - const responseCount = await getResponseCountAction(surveyId, filters); + let responseCount = 0; + + if (isSharingPage) { + responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters); + } else { + responseCount = await getResponseCountAction(surveyId, filters); + } + setResponseCount(responseCount); }; handleResponsesCount(); - }, [filters, surveyId]); + }, [filters, isSharingPage, sharingKey, surveyId]); useEffect(() => { const fetchInitialResponses = async () => { try { setFetchingFirstPage(true); - const responses = await getResponsesAction(surveyId, 1, responsesPerPage, filters); + + let responses: TResponse[] = []; + + if (isSharingPage) { + responses = await getResponsesBySurveySharingKeyAction(sharingKey, 1, responsesPerPage, filters); + } else { + responses = await getResponsesAction(surveyId, 1, responsesPerPage, filters); + } + if (responses.length < responsesPerPage) { setHasMore(false); } @@ -120,7 +156,7 @@ const ResponsePage = ({ } }; fetchInitialResponses(); - }, [surveyId, filters, responsesPerPage]); + }, [surveyId, filters, responsesPerPage, sharingKey, isSharingPage]); useEffect(() => { setPage(1); @@ -141,7 +177,7 @@ const ResponsePage = ({ />
- + {!isSharingPage && }
); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx index b4f6241608..c938756c99 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx @@ -1,9 +1,9 @@ "use client"; import { EmptyInAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; -import React, { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; -import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole"; +import { getMembershipByUserIdTeamIdAction } from "@formbricks/lib/membership/hooks/actions"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { TEnvironment } from "@formbricks/types/environment"; import { TResponse } from "@formbricks/types/responses"; @@ -19,7 +19,7 @@ interface ResponseTimelineProps { surveyId: string; responses: TResponse[]; survey: TSurvey; - user: TUser; + user?: TUser; environmentTags: TTag[]; fetchNextPage: () => void; hasMore: boolean; @@ -28,6 +28,7 @@ interface ResponseTimelineProps { isFetchingFirstPage: boolean; responseCount: number | null; totalResponseCount: number; + isSharingPage?: boolean; } export default function ResponseTimeline({ @@ -43,7 +44,9 @@ export default function ResponseTimeline({ isFetchingFirstPage, responseCount, totalResponseCount, + isSharingPage = false, }: ResponseTimelineProps) { + const [isViewer, setIsViewer] = useState(false); const loadingRef = useRef(null); useEffect(() => { @@ -69,8 +72,16 @@ export default function ResponseTimeline({ }; }, [fetchNextPage, hasMore]); - const { membershipRole } = useMembershipRole(survey.environmentId); - const { isViewer } = getAccessFlags(membershipRole); + useEffect(() => { + const getRole = async () => { + if (isSharingPage) return setIsViewer(true); + + const membershipRole = await getMembershipByUserIdTeamIdAction(survey.environmentId); + const { isViewer } = getAccessFlags(membershipRole); + setIsViewer(isViewer); + }; + getRole(); + }, [survey.environmentId, isSharingPage]); return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx index d825a735f3..f2f7840cdb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx @@ -12,7 +12,11 @@ import SummaryMetadata from "@/app/(app)/environments/[environmentId]/surveys/[s import CustomFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter"; import SummaryHeader from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SummaryHeader"; import { getFormattedFilters } from "@/app/lib/surveys/surveys"; -import { useSearchParams } from "next/navigation"; +import { + getResponseCountBySurveySharingKeyAction, + getSummaryBySurveySharingKeyAction, +} from "@/app/share/[sharingKey]/action"; +import { useParams, useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall"; @@ -48,7 +52,7 @@ interface SummaryPageProps { surveyId: string; webAppUrl: string; product: TProduct; - user: TUser; + user?: TUser; environmentTags: TTag[]; attributes: TSurveyPersonAttributes; membershipRole?: TMembershipRole; @@ -59,14 +63,18 @@ const SummaryPage = ({ environment, survey, surveyId, - webAppUrl, product, + webAppUrl, user, environmentTags, attributes, membershipRole, totalResponseCount, }: SummaryPageProps) => { + const params = useParams(); + const sharingKey = params.sharingKey as string; + const isSharingPage = !!sharingKey; + const [responseCount, setResponseCount] = useState(null); const { selectedFilter, dateRange, resetState } = useResponseFilter(); const [surveySummary, setSurveySummary] = useState(initialSurveySummary); @@ -82,13 +90,25 @@ const SummaryPage = ({ const handleInitialData = async () => { try { setFetchingSummary(true); - const responseCount = await getResponseCountAction(surveyId, filters); + let responseCount; + if (isSharingPage) { + responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters); + } else { + responseCount = await getResponseCountAction(surveyId, filters); + } setResponseCount(responseCount); if (responseCount === 0) { setSurveySummary(initialSurveySummary); return; } - const response = await getSurveySummaryAction(surveyId, filters); + + let response; + if (isSharingPage) { + response = await getSummaryBySurveySharingKeyAction(sharingKey, filters); + } else { + response = await getSurveySummaryAction(surveyId, filters); + } + setSurveySummary(response); } finally { setFetchingSummary(false); @@ -96,7 +116,7 @@ const SummaryPage = ({ }; handleInitialData(); - }, [filters, surveyId]); + }, [filters, isSharingPage, sharingKey, surveyId]); const searchParams = useSearchParams(); @@ -123,7 +143,7 @@ const SummaryPage = ({ />
- + {!isSharingPage && }
{ }; const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps) => { + const params = useParams(); + const isSharingPage = !!params.sharingKey; + const { selectedFilter, setSelectedOptions, dateRange, setDateRange, resetState } = useResponseFilter(); const [filterRange, setFilterRange] = useState( dateRange.from && dateRange.to @@ -264,55 +268,58 @@ const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps - { - value && handleDatePickerClose(); - setIsDownloadDropDownOpen(value); - }}> - -
-
- Download - {isDownloadDropDownOpen ? ( - - ) : ( - - )} + {!isSharingPage && ( + { + value && handleDatePickerClose(); + setIsDownloadDropDownOpen(value); + }}> + +
+
+ Download + {isDownloadDropDownOpen ? ( + + ) : ( + + )} +
+
- -
- - - { - handleDowndloadResponses(FilterDownload.ALL, "csv"); - }}> -

All responses (CSV)

-
- { - handleDowndloadResponses(FilterDownload.ALL, "xlsx"); - }}> -

All responses (Excel)

-
- { - handleDowndloadResponses(FilterDownload.FILTER, "csv"); - }}> -

Current selection (CSV)

-
- { - handleDowndloadResponses(FilterDownload.FILTER, "xlsx"); - }}> -

Current selection (Excel)

-
-
- + + + + { + handleDowndloadResponses(FilterDownload.ALL, "csv"); + }}> +

All responses (CSV)

+
+ { + handleDowndloadResponses(FilterDownload.ALL, "xlsx"); + }}> +

All responses (Excel)

+
+ { + handleDowndloadResponses(FilterDownload.FILTER, "csv"); + }}> +

Current selection (CSV)

+
+ { + handleDowndloadResponses(FilterDownload.FILTER, "xlsx"); + }}> +

Current selection (Excel)

+
+
+ + )}
{isDatePickerOpen && (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton.tsx index 202824a1f3..168afa43c8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton.tsx @@ -25,7 +25,7 @@ import ShareSurveyResults from "../(analysis)/summary/components/ShareSurveyResu interface ResultsShareButtonProps { survey: TSurvey; webAppUrl: string; - user: TUser; + user?: TUser; } export default function ResultsShareButton({ survey, webAppUrl, user }: ResultsShareButtonProps) { @@ -138,7 +138,7 @@ export default function ResultsShareButton({ survey, webAppUrl, user }: ResultsS - {showLinkModal && ( + {showLinkModal && user && ( { + const params = useParams(); + const sharingKey = params.sharingKey as string; + const isSharingPage = !!sharingKey; + const router = useRouter(); const isCloseOnDateEnabled = survey.closeOnDate !== null; @@ -64,135 +68,142 @@ const SummaryHeader = ({

{survey.name}

- {survey.resultShareKey && } + {survey.resultShareKey && !isSharingPage && ( + + )}
{product.name}
-
- {/* */} - {!isViewer && - (environment?.widgetSetupCompleted || survey.type === "link") && - survey?.status !== "draft" ? ( - - ) : null} - {survey.type === "link" && ( - - )} - {!isViewer && ( - - )} -
-
- - - - - - {survey.type === "link" && ( - <> - - - - )} - {(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? ( - <> - - -
- {(survey.type === "link" || environment.widgetSetupCompleted) && ( - - )} - - {survey.status === "scheduled" && "Scheduled"} - {survey.status === "inProgress" && "In-progress"} - {survey.status === "paused" && "Paused"} - {survey.status === "completed" && "Completed"} - -
-
- - - { - const castedValue = value as "draft" | "inProgress" | "paused" | "completed"; - updateSurveyAction({ ...survey, status: castedValue }) - .then(() => { - toast.success( - value === "inProgress" - ? "Survey live" - : value === "paused" - ? "Survey paused" - : value === "completed" - ? "Survey completed" - : "" - ); - router.refresh(); - }) - .catch((error) => { - toast.error(`Error: ${error.message}`); - }); - }}> - - In-progress - - - - Paused - - - - Completed - - - - -
- - + {!isSharingPage && ( + <> +
+ {!isViewer && + (environment.widgetSetupCompleted || survey.type === "link") && + survey.status !== "draft" ? ( + ) : null} - - - -
- - {showShareSurveyModal && ( - + {survey.type === "link" && ( + + )} + {!isViewer && ( + + )} +
+
+ + + + + + {survey.type === "link" && user && ( + <> + + + + )} + {(environment?.widgetSetupCompleted || survey.type === "link") && + survey?.status !== "draft" ? ( + <> + + +
+ {(survey.type === "link" || environment.widgetSetupCompleted) && ( + + )} + + {survey.status === "inProgress" && "In-progress"} + {survey.status === "paused" && "Paused"} + {survey.status === "completed" && "Completed"} + +
+
+ + + { + const castedValue = value as "draft" | "inProgress" | "paused" | "completed"; + updateSurveyAction({ ...survey, status: castedValue }) + .then(() => { + toast.success( + value === "inProgress" + ? "Survey live" + : value === "paused" + ? "Survey paused" + : value === "completed" + ? "Survey completed" + : "" + ); + router.refresh(); + }) + .catch((error) => { + toast.error(`Error: ${error.message}`); + }); + }}> + + In-progress + + + + Paused + + + + Completed + + + + +
+ + + ) : null} + +
+
+
+ {user && ( + + )} + {showShareSurveyModal && user && ( + + )} + )}
); diff --git a/apps/web/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs.tsx b/apps/web/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs.tsx deleted file mode 100644 index 23648914c1..0000000000 --- a/apps/web/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import revalidateSurveyIdPath from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"; -import { InboxIcon, PresentationIcon } from "lucide-react"; -import Link from "next/link"; - -import { cn } from "@formbricks/lib/cn"; - -interface SurveyResultsTabProps { - activeId: string; - environmentId: string; - surveyId: string; - responseCount: number | null; - sharingKey: string; -} - -export default function SurveyResultsTab({ - activeId, - environmentId, - surveyId, - responseCount, - sharingKey, -}: SurveyResultsTabProps) { - const tabs = [ - { - id: "summary", - label: "Summary", - icon: , - href: `/share/${sharingKey}/summary?referer=true`, - }, - { - id: "responses", - label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`, - icon: , - href: `/share/${sharingKey}/responses?referer=true`, - }, - ]; - - return ( -
-
- -
-
- ); -} diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/actions.ts b/apps/web/app/share/[sharingKey]/(analysis)/responses/actions.ts deleted file mode 100644 index 9a0afd22d1..0000000000 --- a/apps/web/app/share/[sharingKey]/(analysis)/responses/actions.ts +++ /dev/null @@ -1,40 +0,0 @@ -"use server"; - -import { getServerSession } from "next-auth"; - -import { authOptions } from "@formbricks/lib/authOptions"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; -import { createTag } from "@formbricks/lib/tag/service"; -import { canUserAccessTagOnResponse } from "@formbricks/lib/tagOnResponse/auth"; -import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service"; -import { AuthorizationError } from "@formbricks/types/errors"; - -export const createTagAction = async (environmentId: string, tagName: string) => { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); - - const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - - return await createTag(environmentId, tagName); -}; - -export const createTagToResponeAction = async (responseId: string, tagId: string) => { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); - - const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - - return await addTagToRespone(responseId, tagId); -}; - -export const deleteTagOnResponseAction = async (responseId: string, tagId: string) => { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); - - const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - - return await deleteTagOnResponse(responseId, tagId); -}; diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx deleted file mode 100644 index b66d863faf..0000000000 --- a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx +++ /dev/null @@ -1,144 +0,0 @@ -"use client"; - -import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import { getFormattedFilters } from "@/app/lib/surveys/surveys"; -import SurveyResultsTabs from "@/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs"; -import ResponseTimeline from "@/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline"; -import { - getResponseCountBySurveySharingKeyAction, - getResponsesBySurveySharingKeyAction, -} from "@/app/share/[sharingKey]/action"; -import CustomFilter from "@/app/share/[sharingKey]/components/CustomFilter"; -import SummaryHeader from "@/app/share/[sharingKey]/components/SummaryHeader"; -import { useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TProduct } from "@formbricks/types/product"; -import { TResponse, TSurveyPersonAttributes } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys"; -import { TTag } from "@formbricks/types/tags"; -import ContentWrapper from "@formbricks/ui/ContentWrapper"; - -interface ResponsePageProps { - environment: TEnvironment; - survey: TSurvey; - surveyId: string; - webAppUrl: string; - product: TProduct; - sharingKey: string; - environmentTags: TTag[]; - attributes: TSurveyPersonAttributes; - responsesPerPage: number; - totalResponseCount: number; -} - -const ResponsePage = ({ - environment, - survey, - surveyId, - product, - sharingKey, - environmentTags, - attributes, - responsesPerPage, - totalResponseCount, -}: ResponsePageProps) => { - const [responses, setResponses] = useState([]); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [responseCount, setResponseCount] = useState(null); - const [isFetchingFirstPage, setFetchingFirstPage] = useState(true); - - const { selectedFilter, dateRange, resetState } = useResponseFilter(); - - const filters = useMemo( - () => getFormattedFilters(survey, selectedFilter, dateRange), - [survey, selectedFilter, dateRange] - ); - - const searchParams = useSearchParams(); - - survey = useMemo(() => { - return checkForRecallInHeadline(survey, "default"); - }, [survey]); - - const fetchNextPage = useCallback(async () => { - const newPage = page + 1; - const newResponses = await getResponsesBySurveySharingKeyAction( - sharingKey, - newPage, - responsesPerPage, - filters - ); - if (newResponses.length === 0 || newResponses.length < responsesPerPage) { - setHasMore(false); - } - setResponses([...responses, ...newResponses]); - setPage(newPage); - }, [filters, page, responses, responsesPerPage, sharingKey]); - - useEffect(() => { - if (!searchParams?.get("referer")) { - resetState(); - } - }, [searchParams, resetState]); - - useEffect(() => { - const handleResponsesCount = async () => { - const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters); - setResponseCount(responseCount); - }; - handleResponsesCount(); - }, [filters, sharingKey]); - - useEffect(() => { - const fetchInitialResponses = async () => { - try { - setFetchingFirstPage(true); - const responses = await getResponsesBySurveySharingKeyAction( - sharingKey, - 1, - responsesPerPage, - filters - ); - if (responses.length < responsesPerPage) { - setHasMore(false); - } - setResponses(responses); - } finally { - setFetchingFirstPage(false); - } - }; - fetchInitialResponses(); - }, [filters, responsesPerPage, sharingKey]); - - return ( - - - - - - - ); -}; - -export default ResponsePage; diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx deleted file mode 100644 index 338b25aaf6..0000000000 --- a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { EmptyInAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; -import { useEffect, useRef } from "react"; - -import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TResponse } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys"; -import { TTag } from "@formbricks/types/tags"; -import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller"; -import SingleResponseCard from "@formbricks/ui/SingleResponseCard"; -import { SkeletonLoader } from "@formbricks/ui/SkeletonLoader"; - -interface ResponseTimelineProps { - environment: TEnvironment; - surveyId: string; - responses: TResponse[]; - survey: TSurvey; - environmentTags: TTag[]; - fetchNextPage: () => void; - hasMore: boolean; - isFetchingFirstPage: boolean; - responseCount: number | null; - totalResponseCount: number; -} - -export default function ResponseTimeline({ - environment, - responses, - survey, - environmentTags, - fetchNextPage, - hasMore, - isFetchingFirstPage, - responseCount, - totalResponseCount, -}: ResponseTimelineProps) { - const loadingRef = useRef(null); - - useEffect(() => { - const currentLoadingRef = loadingRef.current; - - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - if (hasMore) fetchNextPage(); - } - }, - { threshold: 0.8 } - ); - - if (currentLoadingRef) { - observer.observe(currentLoadingRef); - } - - return () => { - if (currentLoadingRef) { - observer.unobserve(currentLoadingRef); - } - }; - }, [fetchNextPage, hasMore]); - - const { membershipRole } = useMembershipRole(survey.environmentId); - const { isViewer } = getAccessFlags(membershipRole); - - return ( -
- {survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? ( - - ) : isFetchingFirstPage ? ( - - ) : responseCount === 0 ? ( - - ) : ( -
- {responses.map((response) => { - return ( -
- -
- ); - })} -
-
- )} -
- ); -} diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx index df69bb2a57..23dfc7eaa1 100644 --- a/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx +++ b/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx @@ -1,15 +1,13 @@ -import ResponsePage from "@/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage"; +import ResponsePage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; import { notFound } from "next/navigation"; -import { RESPONSES_PER_PAGE, REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants"; +import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service"; import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service"; import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; -export const revalidate = REVALIDATION_INTERVAL; - export default async function Page({ params }) { const surveyId = await getSurveyIdByResultShareKey(params.sharingKey); @@ -45,7 +43,6 @@ export default async function Page({ params }) { surveyId={surveyId} webAppUrl={WEBAPP_URL} product={product} - sharingKey={params.sharingKey} environmentTags={tags} attributes={attributes} responsesPerPage={RESPONSES_PER_PAGE} diff --git a/apps/web/app/share/[sharingKey]/(analysis)/summary/components/SummaryPage.tsx b/apps/web/app/share/[sharingKey]/(analysis)/summary/components/SummaryPage.tsx deleted file mode 100644 index 3268a39ec3..0000000000 --- a/apps/web/app/share/[sharingKey]/(analysis)/summary/components/SummaryPage.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import SummaryDropOffs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs"; -import SummaryList from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList"; -import SummaryMetadata from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata"; -import { getFormattedFilters } from "@/app/lib/surveys/surveys"; -import SurveyResultsTabs from "@/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs"; -import { - getResponseCountBySurveySharingKeyAction, - getSummaryBySurveySharingKeyAction, -} from "@/app/share/[sharingKey]/action"; -import CustomFilter from "@/app/share/[sharingKey]/components/CustomFilter"; -import SummaryHeader from "@/app/share/[sharingKey]/components/SummaryHeader"; -import { useSearchParams } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; - -import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall"; -import { TEnvironment } from "@formbricks/types/environment"; -import { TProduct } from "@formbricks/types/product"; -import { TSurveyPersonAttributes, TSurveySummary } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys"; -import { TTag } from "@formbricks/types/tags"; -import ContentWrapper from "@formbricks/ui/ContentWrapper"; - -const initialSurveySummary: TSurveySummary = { - meta: { - completedPercentage: 0, - completedResponses: 0, - displayCount: 0, - dropOffPercentage: 0, - dropOffCount: 0, - startsPercentage: 0, - totalResponses: 0, - ttcAverage: 0, - }, - dropOff: [], - summary: [], -}; - -interface SummaryPageProps { - environment: TEnvironment; - survey: TSurvey; - surveyId: string; - product: TProduct; - sharingKey: string; - environmentTags: TTag[]; - attributes: TSurveyPersonAttributes; - totalResponseCount: number; -} - -const SummaryPage = ({ - environment, - survey, - surveyId, - product, - sharingKey, - environmentTags, - attributes, - totalResponseCount, -}: SummaryPageProps) => { - const [responseCount, setResponseCount] = useState(null); - - const [surveySummary, setSurveySummary] = useState(initialSurveySummary); - const [showDropOffs, setShowDropOffs] = useState(false); - const [isFetchingSummary, setFetchingSummary] = useState(true); - - const { selectedFilter, dateRange, resetState } = useResponseFilter(); - - const filters = useMemo( - () => getFormattedFilters(survey, selectedFilter, dateRange), - [survey, selectedFilter, dateRange] - ); - - useEffect(() => { - const handleInitialData = async () => { - try { - setFetchingSummary(true); - const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters); - setResponseCount(responseCount); - if (responseCount === 0) { - setSurveySummary(initialSurveySummary); - return; - } - const response = await getSummaryBySurveySharingKeyAction(sharingKey, filters); - setSurveySummary(response); - } finally { - setFetchingSummary(false); - } - }; - - handleInitialData(); - }, [filters, sharingKey]); - - survey = useMemo(() => { - return checkForRecallInHeadline(survey, "default"); - }, [survey]); - - const searchParams = useSearchParams(); - - useEffect(() => { - if (!searchParams?.get("referer")) { - resetState(); - } - }, [searchParams, resetState]); - - return ( - - - - - - {showDropOffs && } - - - - ); -}; - -export default SummaryPage; diff --git a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx index d11eafb7ed..7c757a17aa 100644 --- a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx +++ b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx @@ -1,15 +1,13 @@ -import SummaryPage from "@/app/share/[sharingKey]/(analysis)/summary/components/SummaryPage"; +import SummaryPage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage"; import { notFound } from "next/navigation"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service"; import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service"; import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; -export const revalidate = REVALIDATION_INTERVAL; - export default async function Page({ params }) { const surveyId = await getSurveyIdByResultShareKey(params.sharingKey); @@ -43,7 +41,7 @@ export default async function Page({ params }) { environment={environment} survey={survey} surveyId={survey.id} - sharingKey={params.sharingKey} + webAppUrl={WEBAPP_URL} product={product} environmentTags={tags} attributes={attributes} diff --git a/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx b/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx deleted file mode 100755 index 837c09b1a8..0000000000 --- a/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; - -import { - DateRange, - useResponseFilter, -} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; -import ResponseFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter"; -import { generateQuestionAndFilterOptions, getTodayDate } from "@/app/lib/surveys/surveys"; -import { differenceInDays, format, startOfDay, subDays } from "date-fns"; -import { ChevronDown, ChevronUp } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; - -import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside"; -import { TSurveyPersonAttributes } from "@formbricks/types/responses"; -import { TSurvey } from "@formbricks/types/surveys"; -import { TTag } from "@formbricks/types/tags"; -import { Calendar } from "@formbricks/ui/Calendar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@formbricks/ui/DropdownMenu"; - -enum DateSelected { - FROM = "from", - TO = "to", -} - -enum FilterDropDownLabels { - ALL_TIME = "All time", - LAST_7_DAYS = "Last 7 days", - LAST_30_DAYS = "Last 30 days", - CUSTOM_RANGE = "Custom range...", -} - -interface CustomFilterProps { - environmentTags: TTag[]; - attributes: TSurveyPersonAttributes; - survey: TSurvey; -} - -const getDifferenceOfDays = (from, to) => { - const days = differenceInDays(to, from); - if (days === 7) { - return FilterDropDownLabels.LAST_7_DAYS; - } else if (days === 30) { - return FilterDropDownLabels.LAST_30_DAYS; - } else { - return FilterDropDownLabels.CUSTOM_RANGE; - } -}; - -const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps) => { - const { setSelectedOptions, dateRange, setDateRange } = useResponseFilter(); - const [filterRange, setFilterRange] = useState( - dateRange.from && dateRange.to - ? getDifferenceOfDays(dateRange.from, dateRange.to) - : FilterDropDownLabels.ALL_TIME - ); - const [selectingDate, setSelectingDate] = useState(DateSelected.FROM); - const [isDatePickerOpen, setIsDatePickerOpen] = useState(false); - const [isFilterDropDownOpen, setIsFilterDropDownOpen] = useState(false); - const [hoveredRange, setHoveredRange] = useState(null); - - // when the page loads we get total responses and iterate over the responses and questions, tags and attributes to create the filter options - useEffect(() => { - const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions( - survey, - environmentTags, - attributes - ); - setSelectedOptions({ questionFilterOptions, questionOptions }); - }, [survey, setSelectedOptions, environmentTags, attributes]); - - const datePickerRef = useRef(null); - - const extracMetadataKeys = useCallback((obj, parentKey = "") => { - let keys: string[] = []; - - for (let key in obj) { - if (typeof obj[key] === "object" && obj[key] !== null) { - keys = keys.concat(extracMetadataKeys(obj[key], parentKey + key + " - ")); - } else { - keys.push(parentKey + key); - } - } - - return keys; - }, []); - - const handleDateHoveredChange = (date: Date) => { - if (selectingDate === DateSelected.FROM) { - const startOfRange = new Date(date); - startOfRange.setHours(0, 0, 0, 0); // Set to the start of the selected day - - // Check if the selected date is after the current 'to' date - if (startOfRange > dateRange?.to!) { - return; - } else { - setHoveredRange({ from: startOfRange, to: dateRange.to }); - } - } else { - const endOfRange = new Date(date); - endOfRange.setHours(23, 59, 59, 999); // Set to the end of the selected day - - // Check if the selected date is before the current 'from' date - if (endOfRange < dateRange?.from!) { - return; - } else { - setHoveredRange({ from: dateRange.from, to: endOfRange }); - } - } - }; - - const handleDateChange = (date: Date) => { - if (selectingDate === DateSelected.FROM) { - const startOfRange = new Date(date); - startOfRange.setHours(0, 0, 0, 0); // Set to the start of the selected day - - // Check if the selected date is after the current 'to' date - if (startOfRange > dateRange?.to!) { - const nextDay = new Date(startOfRange); - nextDay.setDate(nextDay.getDate() + 1); - nextDay.setHours(23, 59, 59, 999); - setDateRange({ from: startOfRange, to: nextDay }); - } else { - setDateRange((prevData) => ({ from: startOfRange, to: prevData.to })); - } - setSelectingDate(DateSelected.TO); - } else { - const endOfRange = new Date(date); - endOfRange.setHours(23, 59, 59, 999); // Set to the end of the selected day - - // Check if the selected date is before the current 'from' date - if (endOfRange < dateRange?.from!) { - const previousDay = new Date(endOfRange); - previousDay.setDate(previousDay.getDate() - 1); - previousDay.setHours(0, 0, 0, 0); // Set to the start of the selected day - setDateRange({ from: previousDay, to: endOfRange }); - } else { - setDateRange((prevData) => ({ from: prevData?.from, to: endOfRange })); - } - setIsDatePickerOpen(false); - setSelectingDate(DateSelected.FROM); - } - }; - - const handleDatePickerClose = () => { - setIsDatePickerOpen(false); - setSelectingDate(DateSelected.FROM); - }; - - useClickOutside(datePickerRef, () => handleDatePickerClose()); - - return ( - <> -
-
- - { - value && handleDatePickerClose(); - setIsFilterDropDownOpen(value); - }}> - -
- - {filterRange === FilterDropDownLabels.CUSTOM_RANGE - ? `${dateRange?.from ? format(dateRange?.from, "dd LLL") : "Select first date"} - ${ - dateRange?.to ? format(dateRange.to, "dd LLL") : "Select last date" - }` - : filterRange} - - {isFilterDropDownOpen ? ( - - ) : ( - - )} -
-
- - { - setFilterRange(FilterDropDownLabels.ALL_TIME); - setDateRange({ from: undefined, to: getTodayDate() }); - }}> -

All time

-
- { - setFilterRange(FilterDropDownLabels.LAST_7_DAYS); - setDateRange({ from: startOfDay(subDays(new Date(), 7)), to: getTodayDate() }); - }}> -

Last 7 days

-
- { - setFilterRange(FilterDropDownLabels.LAST_30_DAYS); - setDateRange({ from: startOfDay(subDays(new Date(), 30)), to: getTodayDate() }); - }}> -

Last 30 days

-
- { - setIsDatePickerOpen(true); - setFilterRange(FilterDropDownLabels.CUSTOM_RANGE); - setSelectingDate(DateSelected.FROM); - }}> -

Custom range...

-
-
-
-
- {isDatePickerOpen && ( -
- handleDateChange(date)} - onDayMouseEnter={handleDateHoveredChange} - onDayMouseLeave={() => setHoveredRange(null)} - classNames={{ - day_today: "hover:bg-slate-200 bg-white", - }} - /> -
- )} -
- - ); -}; - -export default CustomFilter; diff --git a/apps/web/app/share/[sharingKey]/components/SummaryHeader.tsx b/apps/web/app/share/[sharingKey]/components/SummaryHeader.tsx deleted file mode 100644 index d55fada362..0000000000 --- a/apps/web/app/share/[sharingKey]/components/SummaryHeader.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { TProduct } from "@formbricks/types/product"; -import { TSurvey } from "@formbricks/types/surveys"; - -interface SummaryHeaderProps { - survey: TSurvey; - product: TProduct; -} -const SummaryHeader = ({ survey, product }: SummaryHeaderProps) => { - return ( -
-
-

{survey.name}

- {product.name} -
-
- ); -}; - -export default SummaryHeader;