diff --git a/apps/web/src/app/(dashboard)/dasboard-layout.tsx b/apps/web/src/app/(dashboard)/dasboard-layout.tsx index 81f70d6..88e9768 100644 --- a/apps/web/src/app/(dashboard)/dasboard-layout.tsx +++ b/apps/web/src/app/(dashboard)/dasboard-layout.tsx @@ -1,8 +1,7 @@ "use client"; import { AppSidebar } from "~/components/AppSideBar"; -import { SidebarInset, SidebarTrigger } from "@usesend/ui/src/sidebar"; -import { SidebarProvider } from "@usesend/ui/src/sidebar"; +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@usesend/ui/src/sidebar"; import { useIsMobile } from "@usesend/ui/src/hooks/use-mobile"; import { UpgradeModal } from "~/components/payments/UpgradeModal"; diff --git a/apps/web/src/components/AppSideBar.tsx b/apps/web/src/components/AppSideBar.tsx index 926eded..f0f58b6 100644 --- a/apps/web/src/components/AppSideBar.tsx +++ b/apps/web/src/components/AppSideBar.tsx @@ -4,6 +4,7 @@ import { BookUser, Code, Cog, + MessageSquare, Globe, LayoutTemplate, Mail, @@ -35,7 +36,7 @@ import { import Link from "next/link"; import { MiniThemeSwitcher, ThemeSwitcher } from "./theme/ThemeSwitcher"; import { useSession } from "next-auth/react"; -import { isSelfHosted } from "~/utils/common"; +import { isCloud, isSelfHosted } from "~/utils/common"; import { usePathname } from "next/navigation"; import { Badge } from "@usesend/ui/src/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@usesend/ui/src/avatar"; @@ -49,6 +50,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@usesend/ui/src/dropdown-menu"; +import { FeedbackDialog } from "./FeedbackDialog"; // General items const generalItems = [ @@ -117,7 +119,7 @@ const settingsItems = [ export function AppSidebar() { const { data: session } = useSession(); - const { state, open } = useSidebar(); + const showFeedback = isCloud(); const pathname = usePathname(); @@ -233,6 +235,18 @@ export function AppSidebar() { + {showFeedback ? ( + + + + Feedback + + } + /> + + ) : null} diff --git a/apps/web/src/components/FeedbackDialog.tsx b/apps/web/src/components/FeedbackDialog.tsx new file mode 100644 index 0000000..65da5c2 --- /dev/null +++ b/apps/web/src/components/FeedbackDialog.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { type KeyboardEvent, type ReactNode, useEffect, useState } from "react"; +import { Button } from "@usesend/ui/src/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@usesend/ui/src/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@usesend/ui/src/form"; +import { Textarea } from "@usesend/ui/src/textarea"; +import { toast } from "@usesend/ui/src/toaster"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { api } from "~/trpc/react"; + +const FeedbackSchema = z.object({ + message: z.string().trim().min(1, "Feedback is required").max(2000), +}); + +export function FeedbackDialog({ trigger }: { trigger?: ReactNode }) { + const [open, setOpen] = useState(false); + const [isMac, setIsMac] = useState(false); + + const form = useForm>({ + resolver: zodResolver(FeedbackSchema), + defaultValues: { + message: "", + }, + }); + + const feedbackMutation = api.feedback.send.useMutation({ + onSuccess: () => { + toast.success("Thanks for sharing your feedback!"); + form.reset(); + setOpen(false); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + const messageValue = form.watch("message"); + const trimmedMessage = messageValue?.trim() ?? ""; + + useEffect(() => { + const platform = navigator.userAgent || navigator.platform || "unknown"; + setIsMac(/Mac|iPhone|iPod|iPad/i.test(platform)); + }, []); + + function handleOpenChange(nextOpen: boolean) { + setOpen(nextOpen); + if (!nextOpen) { + form.reset(); + } + } + + function onSubmit(values: z.infer) { + feedbackMutation.mutate({ message: values.message.trim() }); + } + + function handleKeyDown(event: KeyboardEvent) { + const isSubmitShortcut = + (event.metaKey || event.ctrlKey) && event.key === "Enter"; + + if (feedbackMutation.isPending || !isSubmitShortcut) return; + + event.preventDefault(); + form.handleSubmit(onSubmit)(); + } + + return ( + + + {trigger ?? ( + + )} + + + + Send feedback + + Share any thoughts or issues. Your message goes straight to our + founders. + + + +
+ + ( + + +