chore: Simpler Sharing Page [Concept] (#2361)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2024-04-09 14:27:28 +05:30
committed by GitHub
parent 214917cdb8
commit 015d4c7663
16 changed files with 298 additions and 960 deletions
@@ -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: <PresentationIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/surveys/${surveyId}/summary?referer=true`,
href: `${url}/summary?referer=true`,
},
{
id: "responses",
label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`,
icon: <InboxIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/surveys/${surveyId}/responses?referer=true`,
href: `${url}/responses?referer=true`,
},
];
@@ -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<number | null>(null);
const [responses, setResponses] = useState<TResponse[]>([]);
const [page, setPage] = useState<number>(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 = ({
/>
<div className="flex gap-1.5">
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />}
</div>
<SurveyResultsTabs
activeId="responses"
@@ -163,6 +199,7 @@ const ResponsePage = ({
isFetchingFirstPage={isFetchingFirstPage}
responseCount={responseCount}
totalResponseCount={totalResponseCount}
isSharingPage={isSharingPage}
/>
</ContentWrapper>
);
@@ -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 (
<div className="space-y-4">
@@ -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<number | null>(null);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(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 = ({
/>
<div className="flex gap-1.5">
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />}
</div>
<SurveyResultsTabs
activeId="summary"
@@ -12,6 +12,7 @@ import {
} from "@/app/lib/surveys/surveys";
import { differenceInDays, format, startOfDay, subDays } from "date-fns";
import { ChevronDown, ChevronUp, DownloadIcon } from "lucide-react";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast";
@@ -64,6 +65,9 @@ const getDifferenceOfDays = (from, to) => {
};
const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps) => {
const params = useParams();
const isSharingPage = !!params.sharingKey;
const { selectedFilter, setSelectedOptions, dateRange, setDateRange, resetState } = useResponseFilter();
const [filterRange, setFilterRange] = useState<FilterDropDownLabels>(
dateRange.from && dateRange.to
@@ -264,55 +268,58 @@ const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
onOpenChange={(value) => {
value && handleDatePickerClose();
setIsDownloadDropDownOpen(value);
}}>
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-none">
<div className="min-w-auto h-auto rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:flex sm:min-w-[11rem] sm:px-6 sm:py-3">
<div className="hidden w-full items-center justify-between sm:flex">
<span className="text-sm text-slate-700">Download</span>
{isDownloadDropDownOpen ? (
<ChevronUp className="ml-2 h-4 w-4 opacity-50" />
) : (
<ChevronDown className="ml-2 h-4 w-4 opacity-50" />
)}
{!isSharingPage && (
<DropdownMenu
onOpenChange={(value) => {
value && handleDatePickerClose();
setIsDownloadDropDownOpen(value);
}}>
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-none">
<div className="min-w-auto h-auto rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:flex sm:min-w-[11rem] sm:px-6 sm:py-3">
<div className="hidden w-full items-center justify-between sm:flex">
<span className="text-sm text-slate-700">Download</span>
{isDownloadDropDownOpen ? (
<ChevronUp className="ml-2 h-4 w-4 opacity-50" />
) : (
<ChevronDown className="ml-2 h-4 w-4 opacity-50" />
)}
</div>
<DownloadIcon className="block h-4 sm:hidden" />
</div>
<DownloadIcon className="block h-4 sm:hidden" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.ALL, "csv");
}}>
<p className="text-slate-700">All responses (CSV)</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.ALL, "xlsx");
}}>
<p className="text-slate-700">All responses (Excel)</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.FILTER, "csv");
}}>
<p className="text-slate-700">Current selection (CSV)</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.FILTER, "xlsx");
}}>
<p className="text-slate-700">Current selection (Excel)</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.ALL, "csv");
}}>
<p className="text-slate-700">All responses (CSV)</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.ALL, "xlsx");
}}>
<p className="text-slate-700">All responses (Excel)</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.FILTER, "csv");
}}>
<p className="text-slate-700">Current selection (CSV)</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
handleDowndloadResponses(FilterDownload.FILTER, "xlsx");
}}>
<p className="text-slate-700">Current selection (Excel)</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
{isDatePickerOpen && (
<div ref={datePickerRef} className="absolute top-full z-50 my-2 rounded-md border bg-white">
@@ -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
</DropdownMenuContent>
</DropdownMenu>
{showLinkModal && (
{showLinkModal && user && (
<ShareEmbedSurvey
survey={survey}
open={showLinkModal}
@@ -5,7 +5,7 @@ import ResultsShareButton from "@/app/(app)/environments/[environmentId]/surveys
import SurveyStatusDropdown from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { CircleEllipsisIcon, ShareIcon, SquarePenIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -39,7 +39,7 @@ interface SummaryHeaderProps {
survey: TSurvey;
webAppUrl: string;
product: TProduct;
user: TUser;
user?: TUser;
membershipRole?: TMembershipRole;
}
const SummaryHeader = ({
@@ -51,6 +51,10 @@ const SummaryHeader = ({
user,
membershipRole,
}: SummaryHeaderProps) => {
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 = ({
<div>
<div className="flex gap-4">
<p className="text-3xl font-bold text-slate-800">{survey.name}</p>
{survey.resultShareKey && <Badge text="Results are public" type="warning" size="normal"></Badge>}
{survey.resultShareKey && !isSharingPage && (
<Badge text="Results are public" type="warning" size="normal"></Badge>
)}
</div>
<span className="text-base font-extralight text-slate-600">{product.name}</span>
</div>
<div className="hidden justify-end gap-x-1.5 sm:flex">
{/* <ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} /> */}
{!isViewer &&
(environment?.widgetSetupCompleted || survey.type === "link") &&
survey?.status !== "draft" ? (
<SurveyStatusDropdown environment={environment} survey={survey} />
) : null}
{survey.type === "link" && (
<Button
variant="secondary"
onClick={() => {
setShowShareSurveyModal(true);
}}>
<ShareIcon className="h-5 w-5" />
</Button>
)}
{!isViewer && (
<Button
variant="darkCTA"
className="h-full w-full px-3 lg:px-6"
href={`/environments/${environment.id}/surveys/${surveyId}/edit`}>
Edit
<SquarePenIcon className="ml-1 h-4" />
</Button>
)}
</div>
<div className="block sm:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="secondary" className="h-full w-full rounded-md p-2">
<CircleEllipsisIcon className="h-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-2">
{survey.type === "link" && (
<>
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
<DropdownMenuSeparator />
</>
)}
{(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={isStatusChangeDisabled}
style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
<div className="flex items-center">
{(survey.type === "link" || environment.widgetSetupCompleted) && (
<SurveyStatusIndicator status={survey.status} />
)}
<span className="ml-1 text-sm text-slate-700">
{survey.status === "scheduled" && "Scheduled"}
{survey.status === "inProgress" && "In-progress"}
{survey.status === "paused" && "Paused"}
{survey.status === "completed" && "Completed"}
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={survey.status}
onValueChange={(value) => {
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}`);
});
}}>
<DropdownMenuRadioItem
value="inProgress"
className="cursor-pointer break-all text-slate-600">
In-progress
</DropdownMenuRadioItem>
<DropdownMenuSeparator />
<DropdownMenuRadioItem
value="paused"
className="cursor-pointer break-all text-slate-600">
Paused
</DropdownMenuRadioItem>
<DropdownMenuSeparator />
<DropdownMenuRadioItem
value="completed"
className="cursor-pointer break-all text-slate-600">
Completed
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
</>
{!isSharingPage && (
<>
<div className="hidden justify-end gap-x-1.5 sm:flex">
{!isViewer &&
(environment.widgetSetupCompleted || survey.type === "link") &&
survey.status !== "draft" ? (
<SurveyStatusDropdown environment={environment} survey={survey} />
) : null}
<Button
variant="darkCTA"
size="sm"
className="flex h-full w-full justify-center px-3 lg:px-6"
href={`/environments/${environment.id}/surveys/${surveyId}/edit`}>
Edit
<SquarePenIcon className="ml-1 h-4" />
</Button>
</DropdownMenuContent>
</DropdownMenu>
</div>
<SuccessMessage environment={environment} survey={survey} webAppUrl={webAppUrl} user={user} />
{showShareSurveyModal && (
<ShareEmbedSurvey
survey={survey}
open={showShareSurveyModal}
setOpen={setShowShareSurveyModal}
webAppUrl={webAppUrl}
user={user}
/>
{survey.type === "link" && (
<Button
variant="secondary"
onClick={() => {
setShowShareSurveyModal(true);
}}>
<ShareIcon className="h-5 w-5" />
</Button>
)}
{!isViewer && (
<Button
variant="darkCTA"
className="h-full w-full px-3 lg:px-6"
href={`/environments/${environment.id}/surveys/${surveyId}/edit`}>
Edit
<SquarePenIcon className="ml-1 h-4" />
</Button>
)}
</div>
<div className="block sm:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="secondary" className="h-full w-full rounded-md p-2">
<CircleEllipsisIcon className="h-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-2">
{survey.type === "link" && user && (
<>
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
<DropdownMenuSeparator />
</>
)}
{(environment?.widgetSetupCompleted || survey.type === "link") &&
survey?.status !== "draft" ? (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={isStatusChangeDisabled}
style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
<div className="flex items-center">
{(survey.type === "link" || environment.widgetSetupCompleted) && (
<SurveyStatusIndicator status={survey.status} />
)}
<span className="ml-1 text-sm text-slate-700">
{survey.status === "inProgress" && "In-progress"}
{survey.status === "paused" && "Paused"}
{survey.status === "completed" && "Completed"}
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={survey.status}
onValueChange={(value) => {
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}`);
});
}}>
<DropdownMenuRadioItem
value="inProgress"
className="cursor-pointer break-all text-slate-600">
In-progress
</DropdownMenuRadioItem>
<DropdownMenuSeparator />
<DropdownMenuRadioItem
value="paused"
className="cursor-pointer break-all text-slate-600">
Paused
</DropdownMenuRadioItem>
<DropdownMenuSeparator />
<DropdownMenuRadioItem
value="completed"
className="cursor-pointer break-all text-slate-600">
Completed
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
</>
) : null}
<Button
variant="darkCTA"
size="sm"
className="flex h-full w-full justify-center px-3 lg:px-6"
href={`/environments/${environment.id}/surveys/${surveyId}/edit`}>
Edit
<SquarePenIcon className="ml-1 h-4" />
</Button>
</DropdownMenuContent>
</DropdownMenu>
</div>
{user && (
<SuccessMessage environment={environment} survey={survey} webAppUrl={webAppUrl} user={user} />
)}
{showShareSurveyModal && user && (
<ShareEmbedSurvey
survey={survey}
open={showShareSurveyModal}
setOpen={setShowShareSurveyModal}
webAppUrl={webAppUrl}
user={user}
/>
)}
</>
)}
</div>
);
@@ -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: <PresentationIcon className="h-5 w-5" />,
href: `/share/${sharingKey}/summary?referer=true`,
},
{
id: "responses",
label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`,
icon: <InboxIcon className="h-5 w-5" />,
href: `/share/${sharingKey}/responses?referer=true`,
},
];
return (
<div>
<div className="mb-7 h-14 w-full border-b">
<nav className="flex h-full items-center space-x-4 justify-self-center" aria-label="Tabs">
{tabs.map((tab) => (
<Link
key={tab.id}
onClick={() => {
revalidateSurveyIdPath(environmentId, surveyId);
}}
href={tab.href}
className={cn(
tab.id === activeId
? " border-brand-dark text-brand-dark border-b-2 font-semibold"
: "text-slate-500 hover:text-slate-700",
"flex h-full items-center px-3 text-sm font-medium"
)}
aria-current={tab.id === activeId ? "page" : undefined}>
{tab.icon && <div className="mr-2 h-5 w-5">{tab.icon}</div>}
{tab.label}
</Link>
))}
</nav>
</div>
</div>
);
}
@@ -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);
};
@@ -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<TResponse[]>([]);
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
const [responseCount, setResponseCount] = useState<number | null>(null);
const [isFetchingFirstPage, setFetchingFirstPage] = useState<boolean>(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 (
<ContentWrapper>
<SummaryHeader survey={survey} product={product} />
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
<SurveyResultsTabs
activeId="responses"
environmentId={environment.id}
surveyId={surveyId}
responseCount={responseCount}
sharingKey={sharingKey}
/>
<ResponseTimeline
environment={environment}
surveyId={surveyId}
responses={responses}
survey={survey}
environmentTags={environmentTags}
fetchNextPage={fetchNextPage}
hasMore={hasMore}
isFetchingFirstPage={isFetchingFirstPage}
responseCount={responseCount}
totalResponseCount={totalResponseCount}
/>
</ContentWrapper>
);
};
export default ResponsePage;
@@ -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 (
<div className="space-y-4">
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : isFetchingFirstPage ? (
<SkeletonLoader type="response" />
) : responseCount === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
emptyMessage={totalResponseCount === 0 ? undefined : "No response matches your filter"}
/>
) : (
<div>
{responses.map((response) => {
return (
<div key={response.id}>
<SingleResponseCard
survey={survey}
response={response}
user={undefined}
environmentTags={environmentTags}
pageType="response"
environment={environment}
isViewer={isViewer}
/>
</div>
);
})}
<div ref={loadingRef}></div>
</div>
)}
</div>
);
}
@@ -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}
@@ -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<number | null>(null);
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const [isFetchingSummary, setFetchingSummary] = useState<boolean>(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 (
<ContentWrapper>
<SummaryHeader survey={survey} product={product} />
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
<SurveyResultsTabs
activeId="summary"
environmentId={environment.id}
surveyId={surveyId}
responseCount={responseCount}
sharingKey={sharingKey}
/>
<SummaryMetadata
survey={survey}
surveySummary={surveySummary.meta}
showDropOffs={showDropOffs}
setShowDropOffs={setShowDropOffs}
/>
{showDropOffs && <SummaryDropOffs dropOff={surveySummary.dropOff} />}
<SummaryList
summary={surveySummary.summary}
responseCount={responseCount}
survey={survey}
environment={environment}
fetchingSummary={isFetchingSummary}
totalResponseCount={totalResponseCount}
/>
</ContentWrapper>
);
};
export default SummaryPage;
@@ -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}
@@ -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<FilterDropDownLabels>(
dateRange.from && dateRange.to
? getDifferenceOfDays(dateRange.from, dateRange.to)
: FilterDropDownLabels.ALL_TIME
);
const [selectingDate, setSelectingDate] = useState<DateSelected>(DateSelected.FROM);
const [isDatePickerOpen, setIsDatePickerOpen] = useState<boolean>(false);
const [isFilterDropDownOpen, setIsFilterDropDownOpen] = useState<boolean>(false);
const [hoveredRange, setHoveredRange] = useState<DateRange | null>(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<HTMLDivElement>(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 (
<>
<div className="relative mb-12 flex justify-between">
<div className="flex justify-stretch gap-x-1.5">
<ResponseFilter />
<DropdownMenu
onOpenChange={(value) => {
value && handleDatePickerClose();
setIsFilterDropDownOpen(value);
}}>
<DropdownMenuTrigger>
<div className="flex h-auto min-w-[8rem] items-center justify-between rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
<span className="text-sm text-slate-700">
{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}
</span>
{isFilterDropDownOpen ? (
<ChevronUp className="ml-2 h-4 w-4 opacity-50" />
) : (
<ChevronDown className="ml-2 h-4 w-4 opacity-50" />
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
setFilterRange(FilterDropDownLabels.ALL_TIME);
setDateRange({ from: undefined, to: getTodayDate() });
}}>
<p className="text-slate-700">All time</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_7_DAYS);
setDateRange({ from: startOfDay(subDays(new Date(), 7)), to: getTodayDate() });
}}>
<p className="text-slate-700">Last 7 days</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_30_DAYS);
setDateRange({ from: startOfDay(subDays(new Date(), 30)), to: getTodayDate() });
}}>
<p className="text-slate-700">Last 30 days</p>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:ring-0"
onClick={() => {
setIsDatePickerOpen(true);
setFilterRange(FilterDropDownLabels.CUSTOM_RANGE);
setSelectingDate(DateSelected.FROM);
}}>
<p className="text-sm text-slate-700 hover:ring-0">Custom range...</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{isDatePickerOpen && (
<div ref={datePickerRef} className="absolute top-full z-50 my-2 rounded-md border bg-white">
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange?.from}
selected={hoveredRange ? hoveredRange : dateRange}
numberOfMonths={2}
onDayClick={(date) => handleDateChange(date)}
onDayMouseEnter={handleDateHoveredChange}
onDayMouseLeave={() => setHoveredRange(null)}
classNames={{
day_today: "hover:bg-slate-200 bg-white",
}}
/>
</div>
)}
</div>
</>
);
};
export default CustomFilter;
@@ -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 (
<div className="mb-11 mt-6 flex flex-wrap items-center justify-between">
<div>
<p className="text-3xl font-bold text-slate-800">{survey.name}</p>
<span className="text-base font-extralight text-slate-600">{product.name}</span>
</div>
</div>
);
};
export default SummaryHeader;