Redesign help menu item (#2710)

* Seperate help menu item and contact menu item

* Add menu items

* Install react-popover

* Render contact only if support keys are present

* Adjust contact support links

* Add translations

* Add embed changes

* Adjust menu if helped is pressed

* Add items to help menu

* Change button color on selection

* Create endpoint

* Create feedback table

* Create migration file

* Write feedback to db

* Remove logs

* Add response message

* Send feedback email

* Disable submit if no rating and after submit

* Add translations

* Fix padding

* Clean up

* Clean up

* Add user feedback email to .env example

* Lint fixes and styles

* Changed onClick function to a named function and fix style

* Fix ids order

* Removed commented code and changed textarea id and name

* Fix id orders

* Change to AND operator

Co-authored-by: Omar López <zomars@me.com>

* Add user relation to feedback

Co-authored-by: Omar López <zomars@me.com>

* Add migration files

* Change rating to strings

* Change rating to strings

* Fix type errors

* WIP success & error messages

* Change success and error to boolans

* Style messages

* Add await

Co-authored-by: Omar López <zomars@me.com>

* Remove duplicate string

* Refactor import statement

Co-authored-by: Omar López <zomars@me.com>

* Change opacity of emojis

* added support@cal.com email for feedback

* Add success toast

* Update .env.example

Co-authored-by: Omar López <zomars@me.com>

* Add tCRP route

* tCRP send email

* tCRP send email

Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
pull/2829/head^2
Joe Au-Yeung 2022-05-24 09:29:39 -04:00 committed by GitHub
parent c8d6c0dbdd
commit 323524b77c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 536 additions and 150 deletions

View File

@ -70,6 +70,10 @@ NEXT_PUBLIC_ZENDESK_KEY=
# Help Scout Config # Help Scout Config
NEXT_PUBLIC_HELPSCOUT_KEY= NEXT_PUBLIC_HELPSCOUT_KEY=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
# This is used so we can bypass emails in auth flows for E2E testing # This is used so we can bypass emails in auth flows for E2E testing
# Set it to "1" if you need to run E2E tests locally # Set it to "1" if you need to run E2E tests locally
NEXT_PUBLIC_IS_E2E= NEXT_PUBLIC_IS_E2E=

View File

@ -10,12 +10,13 @@ import {
MapIcon, MapIcon,
MoonIcon, MoonIcon,
ViewGridIcon, ViewGridIcon,
QuestionMarkCircleIcon,
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import { UserPlan } from "@prisma/client"; import { UserPlan } from "@prisma/client";
import { SessionContextValue, signOut, useSession } from "next-auth/react"; import { SessionContextValue, signOut, useSession } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { Fragment, ReactNode, useEffect } from "react"; import React, { Fragment, ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core"; import { useIsEmbed } from "@calcom/embed-core";
@ -461,8 +462,10 @@ function UserDropdown({ small }: { small?: boolean }) {
}, },
}); });
const utils = trpc.useContext(); const utils = trpc.useContext();
const [helpOpen, setHelpOpen] = useState(false);
return ( return (
<Dropdown> <Dropdown onOpenChange={() => setHelpOpen(false)}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="group flex w-full cursor-pointer appearance-none items-center text-left"> <button className="group flex w-full cursor-pointer appearance-none items-center text-left">
<span <span
@ -504,96 +507,115 @@ function UserDropdown({ small }: { small?: boolean }) {
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent portalled={true}> <DropdownMenuContent portalled={true}>
<DropdownMenuItem> {helpOpen ? (
<a <HelpMenuItem />
onClick={() => { ) : (
mutation.mutate({ away: !user?.away }); <>
utils.invalidateQueries("viewer.me"); <DropdownMenuItem>
}} <a
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"> onClick={() => {
<MoonIcon mutation.mutate({ away: !user?.away });
className={classNames( utils.invalidateQueries("viewer.me");
user?.away }}
? "text-purple-500 group-hover:text-purple-700" className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
: "text-gray-500 group-hover:text-gray-700", <MoonIcon
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3" className={classNames(
)} user?.away
aria-hidden="true" ? "text-purple-500 group-hover:text-purple-700"
/> : "text-gray-500 group-hover:text-gray-700",
{user?.away ? t("set_as_free") : t("set_as_away")} "h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
</a> )}
</DropdownMenuItem> aria-hidden="true"
<DropdownMenuSeparator className="h-px bg-gray-200" /> />
{user?.username && ( {user?.away ? t("set_as_free") : t("set_as_away")}
<DropdownMenuItem> </a>
<a </DropdownMenuItem>
target="_blank" <DropdownMenuSeparator className="h-px bg-gray-200" />
rel="noopener noreferrer" {user?.username && (
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`} <DropdownMenuItem>
className="flex items-center px-4 py-2 text-sm text-gray-700"> <a
<ExternalLinkIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("view_public_page")} target="_blank"
</a> rel="noopener noreferrer"
</DropdownMenuItem> href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
className="flex items-center px-4 py-2 text-sm text-gray-700">
<ExternalLinkIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" />{" "}
{t("view_public_page")}
</a>
</DropdownMenuItem>
)}
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
href="https://cal.com/slack"
target="_blank"
rel="noreferrer"
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
<svg
viewBox="0 0 2447.6 2452.5"
className={classNames(
"text-gray-500 group-hover:text-gray-700",
"mt-0.5 h-4 w-4 flex-shrink-0 ltr:mr-4 rtl:ml-4"
)}
xmlns="http://www.w3.org/2000/svg">
<g clipRule="evenodd" fillRule="evenodd">
<path
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
fill="currentColor"></path>
<path
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
fill="currentColor"></path>
<path
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
fill="currentColor"></path>
<path
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
fill="currentColor"></path>
</g>
</svg>
{t("join_our_slack")}
</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a
target="_blank"
rel="noopener noreferrer"
href="https://cal.com/roadmap"
className="flex items-center px-4 py-2 text-sm text-gray-700">
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
</a>
</DropdownMenuItem>
<button
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900"
onClick={() => setHelpOpen(true)}>
<QuestionMarkCircleIcon
className={classNames(
"text-gray-500 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3"
)}
aria-hidden="true"
/>
{t("help")}
</button>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
className="flex cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
<LogoutIcon
className={classNames(
"text-gray-500 group-hover:text-gray-700",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
)}
aria-hidden="true"
/>
{t("sign_out")}
</a>
</DropdownMenuItem>
</>
)} )}
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
href="https://cal.com/slack"
target="_blank"
rel="noreferrer"
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
<svg
viewBox="0 0 2447.6 2452.5"
className={classNames(
"text-gray-500 group-hover:text-gray-700",
"mt-0.5 h-4 w-4 flex-shrink-0 ltr:mr-4 rtl:ml-4"
)}
xmlns="http://www.w3.org/2000/svg">
<g clipRule="evenodd" fillRule="evenodd">
<path
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
fill="currentColor"></path>
<path
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
fill="currentColor"></path>
<path
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
fill="currentColor"></path>
<path
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
fill="currentColor"></path>
</g>
</svg>
{t("join_our_slack")}
</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a
target="_blank"
rel="noopener noreferrer"
href="https://cal.com/roadmap"
className="flex items-center px-4 py-2 text-sm text-gray-700">
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
</a>
</DropdownMenuItem>
<HelpMenuItem />
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
className="flex cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
<LogoutIcon
className={classNames(
"text-gray-500 group-hover:text-gray-700",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
)}
aria-hidden="true"
/>
{t("sign_out")}
</a>
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</Dropdown> </Dropdown>
); );

View File

@ -0,0 +1,13 @@
import HelpscoutMenuItem from "@ee/lib/helpscout/HelpscoutMenuItem";
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem";
import ZendeskMenuItem from "@ee/lib/zendesk/ZendeskMenuItem";
export default function HelpMenuItem() {
return (
<>
<IntercomMenuItem />
<ZendeskMenuItem />
<HelpscoutMenuItem />
</>
);
}

View File

@ -1,13 +1,191 @@
import HelpscoutMenuItem from "@ee/lib/helpscout/HelpscoutMenuItem"; import { ExternalLinkIcon, ExclamationIcon } from "@heroicons/react/solid";
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem"; import { useState } from "react";
import ZendeskMenuItem from "@ee/lib/zendesk/ZendeskMenuItem";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import classNames from "@lib/classNames";
import { trpc } from "@lib/trpc";
import ContactMenuItem from "./ContactMenuItem";
export default function HelpMenuItem() { export default function HelpMenuItem() {
const [rating, setRating] = useState<null | string>(null);
const [comment, setComment] = useState("");
// const [errorMessage, setErrorMessage] = useState(false);
const [disableSubmit, setDisableSubmit] = useState(true);
const { t } = useLocale();
const mutation = trpc.useMutation("viewer.submitFeedback");
const onRatingClick = (value: string) => {
setRating(value);
setDisableSubmit(false);
};
const sendFeedback = async (rating: string, comment: string) => {
mutation.mutate({ rating: rating, comment: comment });
if (mutation.isSuccess) {
setDisableSubmit(true);
showToast("Thank you, feedback submitted", "success");
}
};
return ( return (
<> <div className="w-full border-gray-300 bg-white shadow-sm md:w-[150%]">
<IntercomMenuItem /> <div className=" w-full p-5">
<ZendeskMenuItem /> <p className="mb-1 text-neutral-500">{t("resources").toUpperCase()}</p>
<HelpscoutMenuItem /> <a
</> href="https://docs.cal.com/"
target="_blank"
className="flex w-full py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
rel="noreferrer">
{t("support_documentation")}
<ExternalLinkIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"ml-1 h-5 w-5 flex-shrink-0 ltr:mr-3"
)}
/>
</a>
<a
href="https://developer.cal.com/"
target="_blank"
className="flex w-full py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
rel="noreferrer">
{t("developer_documentation")}
<ExternalLinkIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"ml-1 h-5 w-5 flex-shrink-0 ltr:mr-3"
)}
/>
</a>
<ContactMenuItem />
</div>
<hr className=" bg-gray-200" />
<div className="w-full p-5">
<p className="mb-1 text-neutral-500">{t("feedback").toUpperCase()}</p>
<p className="flex w-full py-2 text-sm font-medium text-gray-700">{t("comments")}</p>
<textarea
id="comment"
name="comment"
rows={3}
onChange={(event) => setComment(event.target.value)}
className="my-1 block w-full rounded-sm border-gray-300 py-2 pb-2 shadow-sm sm:text-sm"></textarea>
<div className="my-3 flex justify-end">
<button
className={classNames(
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
rating === "Extremely unsatisfied" ? "grayscale-0" : "opacity-50"
)}
onClick={() => onRatingClick("Extremely unsatisfied")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
<path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
/>
<path
fill="#664500"
d="M22 27c0 2.763-1.791 3-4 3-2.21 0-4-.237-4-3 0-2.761 1.79-6 4-6 2.209 0 4 3.239 4 6zm8-12c-.124 0-.25-.023-.371-.072-5.229-2.091-7.372-5.241-7.461-5.374-.307-.46-.183-1.081.277-1.387.459-.306 1.077-.184 1.385.274.019.027 1.93 2.785 6.541 4.629.513.206.763.787.558 1.3-.157.392-.533.63-.929.63zM6 15c-.397 0-.772-.238-.929-.629-.205-.513.044-1.095.557-1.3 4.612-1.844 6.523-4.602 6.542-4.629.308-.456.929-.577 1.387-.27.457.308.581.925.275 1.383-.089.133-2.232 3.283-7.46 5.374C6.25 14.977 6.124 15 6 15z"
/>
<path fill="#5DADEC" d="M24 16h4v19l-4-.046V16zM8 35l4-.046V16H8v19z" />
<path
fill="#664500"
d="M14.999 18c-.15 0-.303-.034-.446-.105-3.512-1.756-7.07-.018-7.105 0-.495.249-1.095.046-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.498-2.197 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552zm14 0c-.15 0-.303-.034-.446-.105-3.513-1.756-7.07-.018-7.105 0-.494.248-1.094.047-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.501-2.196 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552z"
/>
<ellipse fill="#5DADEC" cx="18" cy="34" rx="18" ry="2" />
<ellipse fill="#E75A70" cx="18" cy="27" rx="3" ry="2" />
</svg>
</button>
<button
className={classNames(
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
rating === "Unsatisfied" ? "grayscale-0" : "opacity-50"
)}
onClick={() => onRatingClick("Unsatisfied")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
<path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"
/>
<ellipse fill="#664500" cx="11.5" cy="14.5" rx="2.5" ry="3.5" />
<ellipse fill="#664500" cx="24.5" cy="14.5" rx="2.5" ry="3.5" />
<path
fill="#664500"
d="M8.665 27.871c.178.161.444.171.635.029.039-.029 3.922-2.9 8.7-2.9 4.766 0 8.662 2.871 8.7 2.9.191.142.457.13.635-.029.177-.16.217-.424.094-.628C27.3 27.029 24.212 22 18 22s-9.301 5.028-9.429 5.243c-.123.205-.084.468.094.628z"
/>
</svg>
</button>
<button
className={classNames(
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
rating === "Satisfied" ? "grayscale-0" : "opacity-50"
)}
onClick={() => onRatingClick("Satisfied")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
<path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"
/>
<path
fill="#664500"
d="M28.457 17.797c-.06-.135-1.499-3.297-4.457-3.297-2.957 0-4.397 3.162-4.457 3.297-.092.207-.032.449.145.591.175.142.426.147.61.014.012-.009 1.262-.902 3.702-.902 2.426 0 3.674.881 3.702.901.088.066.194.099.298.099.11 0 .221-.037.312-.109.177-.142.238-.386.145-.594zm-12 0c-.06-.135-1.499-3.297-4.457-3.297-2.957 0-4.397 3.162-4.457 3.297-.092.207-.032.449.144.591.176.142.427.147.61.014.013-.009 1.262-.902 3.703-.902 2.426 0 3.674.881 3.702.901.088.066.194.099.298.099.11 0 .221-.037.312-.109.178-.142.237-.386.145-.594zM18 22c-3.623 0-6.027-.422-9-1-.679-.131-2 0-2 2 0 4 4.595 9 11 9 6.404 0 11-5 11-9 0-2-1.321-2.132-2-2-2.973.578-5.377 1-9 1z"
/>
<path fill="#FFF" d="M9 23s3 1 9 1 9-1 9-1-2 4-9 4-9-4-9-4z" />
</svg>
</button>
<button
className={classNames(
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
rating === "Extremely satisfied" ? "grayscale-0" : "opacity-50"
)}
onClick={() => onRatingClick("Extremely satisfied")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
<path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
/>
<path
fill="#664500"
d="M18 21c-3.623 0-6.027-.422-9-1-.679-.131-2 0-2 2 0 4 4.595 9 11 9 6.404 0 11-5 11-9 0-2-1.321-2.132-2-2-2.973.578-5.377 1-9 1z"
/>
<path fill="#FFF" d="M9 22s3 1 9 1 9-1 9-1-2 4-9 4-9-4-9-4z" />
<path
fill="#E95F28"
d="M15.682 4.413l-4.542.801L8.8.961C8.542.492 8.012.241 7.488.333c-.527.093-.937.511-1.019 1.039l-.745 4.797-4.542.801c-.535.094-.948.525-1.021 1.064s.211 1.063.703 1.297l4.07 1.932-.748 4.812c-.083.536.189 1.064.673 1.309.179.09.371.133.562.133.327 0 .65-.128.891-.372l3.512-3.561 4.518 2.145c.49.232 1.074.123 1.446-.272.372-.395.446-.984.185-1.459L13.625 9.73l3.165-3.208c.382-.387.469-.977.217-1.459-.254-.482-.793-.743-1.325-.65zm4.636 0l4.542.801L27.2.961c.258-.469.788-.72 1.312-.628.526.093.936.511 1.018 1.039l.745 4.797 4.542.801c.536.094.949.524 1.021 1.063s-.211 1.063-.703 1.297l-4.07 1.932.748 4.812c.083.536-.189 1.064-.673 1.309-.179.09-.371.133-.562.133-.327 0-.65-.128-.891-.372l-3.512-3.561-4.518 2.145c-.49.232-1.074.123-1.446-.272-.372-.395-.446-.984-.185-1.459l2.348-4.267-3.165-3.208c-.382-.387-.469-.977-.217-1.459.255-.482.794-.743 1.326-.65z"
/>
</svg>
</button>
</div>
<div className="my-2 flex justify-end">
<Button
disabled={disableSubmit}
loading={mutation.isLoading}
onClick={async () => {
if (rating && comment) {
await sendFeedback(rating, comment);
}
}}>
{t("submit")}
</Button>
</div>
{mutation.isError && (
<div className="mb-4 flex bg-red-100 p-4 text-sm text-red-700">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5" />
</div>
<div className="ml-3 flex-grow">
<p className="font-medium">{t("feedback_error")}</p>
<p>{t("please_try_again")}</p>
</div>
</div>
)}
</div>
</div>
); );
} }

View File

@ -22,20 +22,12 @@ export default function HelpscoutMenuItem() {
else else
return ( return (
<> <>
<DropdownMenuItem> <button
<button onClick={handleClick}
onClick={handleClick} className="flex w-full py-2 pr-4 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900"> {t("contact_support")}
<ChatAltIcon </button>
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3"
)}
aria-hidden="true"
/>
{t("help")}
</button>
</DropdownMenuItem>
{active && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />} {active && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
</> </>
); );

View File

@ -13,22 +13,13 @@ export default function IntercomMenuItem() {
if (!process.env.NEXT_PUBLIC_INTERCOM_APP_ID) return null; if (!process.env.NEXT_PUBLIC_INTERCOM_APP_ID) return null;
else else
return ( return (
<DropdownMenuItem> <button
<button onClick={() => {
onClick={() => { boot();
boot(); show();
show(); }}
}} className="flex w-full py-2 pr-4 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900"> {t("contact_support")}
<ChatAltIcon </button>
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3"
)}
aria-hidden="true"
/>
{t("help")}
</button>
</DropdownMenuItem>
); );
} }

View File

@ -17,20 +17,11 @@ export default function ZendeskMenuItem() {
else else
return ( return (
<> <>
<DropdownMenuItem> <button
<button onClick={() => setActive(true)}
onClick={() => setActive(true)} className="flex w-full py-2 pr-4 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900"> {t("contact_support")}
<ChatAltIcon </button>
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3"
)}
aria-hidden="true"
/>
{t("help")}
</button>
</DropdownMenuItem>
{active && ( {active && (
<Script id="ze-snippet" src={"https://static.zdassets.com/ekr/snippet.js?key=" + ZENDESK_KEY} /> <Script id="ze-snippet" src={"https://static.zdassets.com/ekr/snippet.js?key=" + ZENDESK_KEY} />
)} )}

View File

@ -7,6 +7,7 @@ import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email";
import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email"; import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email";
import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email"; import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email";
import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email"; import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email";
import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email";
import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email"; import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email";
import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email"; import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email";
import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email"; import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email";
@ -266,3 +267,14 @@ export const sendRequestRescheduleEmail = async (
await Promise.all(emailsToSend); await Promise.all(emailsToSend);
}; };
export const sendFeedbackEmail = async (feedback: Feedback) => {
await new Promise((resolve, reject) => {
try {
const feedbackEmail = new FeedbackEmail(feedback);
resolve(feedbackEmail.sendEmail());
} catch (e) {
reject(console.error("FeedbackEmail.sendEmail failed", e));
}
});
};

View File

@ -0,0 +1,118 @@
import BaseEmail from "@lib/emails/templates/_base-email";
import { emailHead, emailBodyLogo } from "./common";
export interface Feedback {
userId: number;
rating: string;
comment: string;
}
export default class FeedbackEmail extends BaseEmail {
feedback: Feedback;
constructor(feedback: Feedback) {
super();
this.feedback = feedback;
}
protected getNodeMailerPayload(): Record<string, unknown> {
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: process.env.SEND_FEEDBACK_EMAIL,
subject: `User Feedback`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
userId: ${this.feedback.userId}
rating: ${this.feedback.rating}
comment: ${this.feedback.comment}
`;
}
protected getHtmlBody(): string {
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead("Feedback")}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">Feedback</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<div style="background-color:#F5F5F5;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
<div style="line-height: 6px;">
<p style="color: #494949;">Used id</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
this.feedback.userId
}</p>
</div>
<div style="line-height: 6px;">
<p style="color: #494949;">Rating</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
this.feedback.rating
}</p>
</div>
<div style="line-height: 6px;">
<p style="color: #494949;">Comment</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
this.feedback.comment
}</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}

View File

@ -813,16 +813,16 @@
"former_time": "Former time", "former_time": "Former time",
"confirmation_page_gif": "Gif for confirmation page", "confirmation_page_gif": "Gif for confirmation page",
"search": "Search", "search": "Search",
"impersonate":"Impersonate", "impersonate": "Impersonate",
"impersonate_user_tip":"All uses of this feature is audited.", "impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning":"Impersonating username \"{{user}}\".", "impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "<0>Click Here to stop</0>.", "impersonating_stop_instructions": "<0>Click Here to stop</0>.",
"email_validation_error":"That doesn't look like an email address", "email_validation_error": "That doesn't look like an email address",
"place_where_cal_widget_appear": "Place this code in your HTML where you want your Cal widget to appear.", "place_where_cal_widget_appear": "Place this code in your HTML where you want your Cal widget to appear.",
"copy_code": "Copy Code", "copy_code": "Copy Code",
"code_copied": "Code copied!", "code_copied": "Code copied!",
"how_you_want_add_cal_site":"How do you want to add Cal to your site?", "how_you_want_add_cal_site": "How do you want to add Cal to your site?",
"choose_ways_put_cal_site":"Choose one of the following ways to put Cal on your site.", "choose_ways_put_cal_site": "Choose one of the following ways to put Cal on your site.",
"setting_up_zapier": "Setting up your Zapier integration", "setting_up_zapier": "Setting up your Zapier integration",
"generate_api_key": "Generate Api Key", "generate_api_key": "Generate Api Key",
"your_unique_api_key": "Your unique API key", "your_unique_api_key": "Your unique API key",
@ -832,6 +832,16 @@
"go_to_app_store": "Go to App Store", "go_to_app_store": "Go to App Store",
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions", "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",
"calendar_no_busy_slots": "There are no busy slots", "calendar_no_busy_slots": "There are no busy slots",
"share_feedback": "Share feedback",
"resources": "Resources",
"support_documentation": "Support documentation",
"developer_documentation": "Developer Documentation",
"get_in_touch": "Get in touch",
"contact_support": "Contact Support",
"feedback": "Feedback",
"submitted_feedback": "Thank you for your feedback!",
"feedback_error": "Error sending feedback",
"comments": "Comments",
"booking_details": "Booking details", "booking_details": "Booking details",
"or_lowercase": "or", "or_lowercase": "or",
"nevermind": "Nevermind", "nevermind": "Nevermind",

View File

@ -1,4 +1,5 @@
import { BookingStatus, MembershipRole, Prisma } from "@prisma/client"; import { BookingStatus, MembershipRole, Prisma } from "@prisma/client";
import dayjs from "dayjs";
import _ from "lodash"; import _ from "lodash";
import { JSONObject } from "superjson/dist/types"; import { JSONObject } from "superjson/dist/types";
import { z } from "zod"; import { z } from "zod";
@ -10,6 +11,7 @@ import { bookingMinimalSelect } from "@calcom/prisma";
import { RecurringEvent } from "@calcom/types/Calendar"; import { RecurringEvent } from "@calcom/types/Calendar";
import { checkRegularUsername } from "@lib/core/checkRegularUsername"; import { checkRegularUsername } from "@lib/core/checkRegularUsername";
import { sendFeedbackEmail } from "@lib/emails/email-manager";
import jackson from "@lib/jackson"; import jackson from "@lib/jackson";
import { import {
isSAMLLoginEnabled, isSAMLLoginEnabled,
@ -891,6 +893,32 @@ const loggedInViewerRouter = createProtectedRouter()
throw new TRPCError({ code: "BAD_REQUEST" }); throw new TRPCError({ code: "BAD_REQUEST" });
} }
}, },
})
.mutation("submitFeedback", {
input: z.object({
rating: z.string(),
comment: z.string(),
}),
async resolve({ input, ctx }) {
const { rating, comment } = input;
const feedback = {
userId: ctx.user.id,
rating: rating,
comment: comment,
};
await ctx.prisma.feedback.create({
data: {
date: dayjs().toISOString(),
userId: ctx.user.id,
rating: rating,
comment: comment,
},
});
if (process.env.SEND_FEEDBACK_EMAIL && comment) sendFeedbackEmail(feedback);
},
}); });
export const viewerRouter = createRouter() export const viewerRouter = createRouter()

View File

@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "Feedback" (
"id" SERIAL NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"userId" INTEGER NOT NULL,
"rating" TEXT NOT NULL,
"comment" TEXT,
CONSTRAINT "Feedback_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,2 @@
-- AddForeignKey
ALTER TABLE "Feedback" ADD CONSTRAINT "Feedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -173,6 +173,7 @@ model User {
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]
Feedback Feedback[]
@@map(name: "users") @@map(name: "users")
} }
@ -477,3 +478,12 @@ model App {
Webhook Webhook[] Webhook Webhook[]
ApiKey ApiKey[] ApiKey ApiKey[]
} }
model Feedback {
id Int @id @default(autoincrement())
date DateTime
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
rating String
comment String?
}

View File

@ -10366,6 +10366,11 @@ libphonenumber-js@^1.9.52, libphonenumber-js@^1.9.53:
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.53.tgz#f4f3321f8fb0ee62952c2a8df4711236d2626088" resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.53.tgz#f4f3321f8fb0ee62952c2a8df4711236d2626088"
integrity sha512-3cuMrA2CY3TbKVC0wKye5dXYgxmVVi4g13gzotprQSguFHMqf0pIrMM2Z6ZtMsSWqvtIqi5TuQhGjMhxz0O9Mw== integrity sha512-3cuMrA2CY3TbKVC0wKye5dXYgxmVVi4g13gzotprQSguFHMqf0pIrMM2Z6ZtMsSWqvtIqi5TuQhGjMhxz0O9Mw==
libphonenumber-js@^1.9.53:
version "1.9.53"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.53.tgz#f4f3321f8fb0ee62952c2a8df4711236d2626088"
integrity sha512-3cuMrA2CY3TbKVC0wKye5dXYgxmVVi4g13gzotprQSguFHMqf0pIrMM2Z6ZtMsSWqvtIqi5TuQhGjMhxz0O9Mw==
lie@3.1.1: lie@3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"