Skip to content

Commit

Permalink
Merge pull request #1178 from The-Commit-Company/help-and-support-menu
Browse files Browse the repository at this point in the history
feat: add help and support page
  • Loading branch information
nikkothari22 authored Dec 18, 2024
2 parents 92f56fa + a4f2cb9 commit 4311992
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
exclude: "node_modules|.git"
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: false

repos:
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const router = createBrowserRouter(
<Route path="create" lazy={() => import('./pages/settings/MessageActions/CreateMessageAction')} />
<Route path=":ID" lazy={() => import('./pages/settings/MessageActions/ViewMessageAction')} />
</Route>

<Route path="help" lazy={() => import('./pages/settings/HelpAndSupport')} />
</Route>
<Route path=":workspaceID" element={<MainPage />}>
<Route index element={<MobileTabsPage />} />
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/components/feature/settings/help/SocketIOHealth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ErrorCallout } from "@/components/common/Callouts/ErrorCallouts"
import { Stack } from "@/components/layout/Stack"
import { Badge, Flex, Heading, IconButton, Link, Text } from "@radix-ui/themes"
import clsx from "clsx"
import { FrappeConfig, FrappeContext, useFrappeEventListener } from "frappe-react-sdk"
import { useContext, useEffect, useState } from "react"
import { LuRefreshCcw } from "react-icons/lu"
import { TbReportAnalytics } from "react-icons/tb"

const SocketIOHealth = () => {

const { socket } = useContext(FrappeContext) as FrappeConfig

const [loading, setLoading] = useState<boolean>(true)
const [socketPingTest, setSocketPingTest] = useState('Fail')
const [socketTransportMode, setSocketTransportMode] = useState<string | undefined>('')

useFrappeEventListener('pong', () => {
setSocketPingTest('Pass')
setLoading(false)
setSocketTransportMode(socket?.io.engine.transport.name)
})

const onPingCheck = () => {
setLoading(true)
socket?.emit('ping')
setTimeout(() => {
setLoading(false)
setSocketTransportMode(s => {
if (!s) {
return ''
}
return s
})
}, 5000)
}

useEffect(() => {
setTimeout(onPingCheck, 5000);
}, []);

return (
<Stack>
<Heading as='h3' size='3' className="not-cal font-semibold">Realtime Connection Test</Heading>
<Text size='2' color='gray'>If messages on Raven do not appear in realtime, you can inspect your network connection here.</Text>
{!loading && socketPingTest === 'Fail' && <ErrorCallout
message="Realtime connections are not working on your site. Messages won't be refreshed in real-time."
/>}
<Flex gap="3" align="center" pt='2'>
<Text size="2" color="gray" as='span'>Real-time Ping Check:</Text>
<Flex align="center" gap="2">
<Badge color={loading ? 'gray' : socketPingTest === "Pass" ? 'green' : 'red'}>{loading ? 'Loading...' : socketPingTest}</Badge>
{!loading && <IconButton title="Send a ping" aria-label="send a ping" color="gray" size="1" variant="ghost" onClick={onPingCheck}>
<LuRefreshCcw className={clsx(loading ? "animate-spin" : null)} size={12} />
</IconButton>}
</Flex>
</Flex>

{socketTransportMode && <Flex gap="2" align="center">
<Text size="2" color="gray" as='span'>SocketIO Transport Mode:</Text>
<Badge color="orange">{socketTransportMode}</Badge>
</Flex>}
<div className="pt-2">
<Link underline="always" size='2' target="_blank"
title="System Health Report"
href="/app/system-health-report"><TbReportAnalytics size='16' className="-mb-0.5 pr-1" />View Full System Health Report
</Link>
</div>
</Stack>
)
}

export default SocketIOHealth
203 changes: 203 additions & 0 deletions frontend/src/components/feature/settings/help/SupportRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { ReactNode } from "react";
import { Box, Button, Dialog, Flex, Link, RadioCards, Text, TextArea, TextField } from "@radix-ui/themes"
import { Controller, useForm } from "react-hook-form"
import { MdOutlineMessage, MdOutlineQuestionMark } from "react-icons/md";
import clsx from "clsx";
import { useFrappePostCall } from "frappe-react-sdk";
import { toast } from "sonner";
import { BiBug } from "react-icons/bi";
import { ErrorText, Label } from "@/components/common/Form"
import { Loader } from "@/components/common/Loader"
import { HStack, Stack } from "@/components/layout/Stack"
import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog"
import { useUserData } from "@/hooks/useUserData";

type TicketType = "Feedback" | "Question" | "Bug"

const subTitles: Record<TicketType, { heading: string, subHeading: string, defaultTextAreaValue: string, footerHeading: ReactNode }> = {
"Feedback": {
heading: "Send feedback",
subHeading: "How can we improve Linear? If you have a feature request, can you also share how you would use it and why it's important to you?",
defaultTextAreaValue: "What if...",
footerHeading: <span>You can also email us at <Link href="mailto:support@thecommit.company" underline="none" size='1' target="_blank">support@thecommit.company</Link> We can't respond to every request but we read all of them.</span>
},
"Question": {
heading: "Ask a question",
subHeading: "How can we help? Please share any relevant information we may need to answer your question.",
defaultTextAreaValue: "How do I...",
footerHeading: <span>You can also email us at <Link href="mailto:support@thecommit.company" underline="none" size='1' target="_blank">support@thecommit.company</Link></span>
},
"Bug": {
heading: "Contact us",
subHeading: "What is the issue? If you're reporting a bug, what are the steps you took so we can reproduce the behaviour?",
defaultTextAreaValue: "Something seems wrong...",
footerHeading: <span>You can also email us at <Link href="mailto:support@thecommit.company" underline="none" size='1' target="_blank">support@thecommit.company</Link></span>
},
};

interface CreateSupportTicketDialogProps {
open: boolean
onClose: VoidFunction
}

const CreateSupportTicketDialog = ({ open, onClose }: CreateSupportTicketDialogProps) => {

return (
<Dialog.Root open={open} onOpenChange={onClose}>
<Dialog.Content maxWidth={'700px'} className={clsx(DIALOG_CONTENT_CLASS)}>
<SupportRequestForm onClose={onClose} />
</Dialog.Content>
</Dialog.Root>
)
}

interface SupportRequestFormFields {
ticket_type: TicketType
email: string
description: string
}
interface SupportRequestFormProps {
onClose: () => void
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const SupportRequestForm = ({ onClose }: SupportRequestFormProps) => {

const { name } = useUserData()

const {
control,
register,
formState: { errors },
handleSubmit,
watch
} = useForm<SupportRequestFormFields>({
defaultValues: {
ticket_type: "Feedback",
email: emailRegex.test(name ?? '') ? name : ""
}
})

const requestType = watch("ticket_type")

const { call, error, loading } = useFrappePostCall('raven.api.support_request.submit_support_request')

const onSubmit = (data: SupportRequestFormFields) => {
call({
email: data.email,
ticket_type: data.ticket_type,
subject: data.description.substring(0, 140),
description: data.description
})
.then(() => {
toast.success("Form submitted successfully!")
onClose()
})
}

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Dialog.Title>{subTitles[requestType].heading}</Dialog.Title>
<Dialog.Description size={'2'} className="min-h-10 select-none">{subTitles[requestType].subHeading}</Dialog.Description>
<Stack gap="2" pt="3">
<Controller
name="ticket_type"
control={control}
rules={{
required: 'Request type is required',
validate: (value) => Object.keys(subTitles).includes(value) || 'Invalid request type'
}}
render={({ field: { onChange, value } }) => (
<RadioCards.Root
value={value}
onValueChange={onChange}
>
<RadioCards.Item value="Feedback">
<Flex direction="column" width="100%" gap="1">
<Flex align="center" gap="3">
<MdOutlineMessage size="16" />
<Text weight="bold">Feedback</Text>
</Flex>
</Flex>
</RadioCards.Item>
<RadioCards.Item value="Question">
<Flex direction="column" width="100%" gap="1">
<Flex align="center" gap="3">
<MdOutlineQuestionMark size="16" />
<Text weight="bold">Question</Text>
</Flex>
</Flex>
</RadioCards.Item>
<RadioCards.Item value="Bug">
<Flex direction="column" width="100%" gap="1">
<Flex align="center" gap="3">
<BiBug size="16" />
<Text weight="bold">Bug</Text>
</Flex>
</Flex>
</RadioCards.Item>
</RadioCards.Root>
)}
/>

{errors.ticket_type && <ErrorText>{errors.ticket_type.message}</ErrorText>}

<Stack>
<Box>
<Label htmlFor='email' isRequired>Email</Label>
<TextField.Root
id='email'
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address"
}
})}
type="email"
placeholder="email@example.com"
aria-invalid={errors.email ? 'true' : 'false'}
/>
</Box>
{errors.email && <ErrorText>{errors.email?.message}</ErrorText>}
</Stack>

<Stack>
<Box>
<Label htmlFor='description' isRequired>Description</Label>
<TextArea
id='description'
{...register('description', {
required: 'Description is required',
minLength: {
value: 10,
message: 'Description must be at least 10 characters'
}
})}
rows={5}
resize='vertical'
placeholder={subTitles[requestType].defaultTextAreaValue}
aria-invalid={errors.description ? 'true' : 'false'}
/>
</Box>
{errors.description && <ErrorText>{errors.description?.message}</ErrorText>}
</Stack>

<HStack justify="between" pt='4' gap="9">
<Text color="gray" size="1" className="select-none">{subTitles[requestType].footerHeading}</Text>
<Flex gap="2">
<Dialog.Close>
<Button color='gray' variant={'soft'} disabled={loading && !error}>Cancel</Button>
</Dialog.Close>
<Button type="submit" disabled={loading} >
{loading && !error ? <Loader className="text-white" /> : null}
Submit
</Button>
</Flex>
</HStack>
</Stack>
</form>
);
}

export default CreateSupportTicketDialog
15 changes: 9 additions & 6 deletions frontend/src/components/feature/userSettings/SettingsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { __ } from '@/utils/translations'
import { Box, Flex, Separator, Text } from '@radix-ui/themes'
import clsx from 'clsx'
import { PropsWithChildren, createElement } from 'react'
import { PropsWithChildren, createElement } from 'react';
import { IconType } from 'react-icons'
import { BiBot, BiBuildings } from 'react-icons/bi'
import { BiBot, BiBuildings, BiHelpCircle } from 'react-icons/bi'
import { BsBoxes } from 'react-icons/bs'
import { FiHelpCircle, FiLifeBuoy } from 'react-icons/fi';
import { LuCircleUserRound } from 'react-icons/lu'
import { NavLink } from 'react-router-dom'

export const SettingsSidebar = () => {

return (
<Box className="h-[calc(100vh-57px)] fixed w-64 border-r pt-2 border-gray-4 dark:border-gray-4">
<Flex direction="column" gap='2' className='px-4'>
Expand Down Expand Up @@ -41,6 +41,8 @@ export const SettingsSidebar = () => {
<SettingsSidebarItem title="Commands" to='commands' />
<SettingsSidebarItem title="OpenAI Settings" to='openai-settings' />
</SettingsGroup>
<SettingsSeparator />
<SettingsSidebarItem title="Help & Support" to='help' standalone icon={FiLifeBuoy} />
</Flex>
</Box>
)
Expand All @@ -65,7 +67,7 @@ const SettingsSidebarGroupHeader = ({ title, icon }: { title: string, icon: Icon
)
}

const SettingsSidebarItem = ({ title, to, end }: { title: string, to: string, end?: boolean }) => {
const SettingsSidebarItem = ({ title, to, end, standalone = false, icon }: { title: string, to: string, end?: boolean, standalone?: boolean, icon?: IconType }) => {

const activeClass = "bg-slate-3 dark:bg-slate-4 hover:bg-slate-3 hover:dark:bg-slate-4"

Expand All @@ -77,8 +79,9 @@ const SettingsSidebarItem = ({ title, to, end }: { title: string, to: string, en
>
{({ isActive }) => {
return (
<Box className='ml-4'>
<Flex className={clsx(`px-2 py-1 text-gray-12 rounded-md w-full`, isActive ? activeClass : "bg-transparent hover:bg-slate-2 hover:dark:bg-slate-3")}>
<Box className={!standalone ? 'ml-4' : ''}>
<Flex className={clsx(`px-2 py-1 text-gray-12 rounded-md w-full items-center`, isActive ? activeClass : "bg-transparent hover:bg-slate-2 hover:dark:bg-slate-3", standalone ? "gap-1.5" : '')}>
{icon ? createElement(icon, { size: 15 }) : null}
<Text className='text-[13px]' weight='medium'>{__(title)}</Text>
</Flex>
</Box>
Expand Down
Loading

0 comments on commit 4311992

Please sign in to comment.