diff --git a/src/components/FeedbackCreateForm/FeedbackCreateForm.tsx b/src/components/FeedbackCreateForm/FeedbackCreateForm.tsx index 2bef05bcf..f91672833 100644 --- a/src/components/FeedbackCreateForm/FeedbackCreateForm.tsx +++ b/src/components/FeedbackCreateForm/FeedbackCreateForm.tsx @@ -7,13 +7,14 @@ import * as Sentry from '@sentry/nextjs'; import { errorsProvider } from '../../utils/forms'; import { createFeedbackSchema, CreateFeedback } from '../../schema/feedback'; import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal'; -import { dispatchErrorNotification } from '../../utils/dispatchNotification'; import { notifyPromise } from '../../utils/notifyPromise'; +import { trpc } from '../../utils/trpcClient'; import { tr } from './FeedbackCreateForm.i18n'; const FeedbackCreateForm: React.FC = () => { const [formBusy, setFormBusy] = useState(false); + const createMutation = trpc.feedback.create.useMutation(); const { register, @@ -25,28 +26,25 @@ const FeedbackCreateForm: React.FC = () => { const errorsResolver = errorsProvider(errors, isSubmitted); - const onPending = useCallback(async (form: CreateFeedback) => { - setFormBusy(true); - const [res] = await notifyPromise( - fetch('/api/feedback', { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: 'application/json', - }, - body: JSON.stringify({ + const onPending = useCallback( + async (form: CreateFeedback) => { + setFormBusy(true); + const [res] = await notifyPromise( + createMutation.mutateAsync({ title: form.title, description: form.description, href: window.location.href, }), - }), - 'sentFeedback', - ); - if (res) { - dispatchModalEvent(ModalEvent.FeedbackCreateModal)(); - } - setFormBusy(false); - }, []); + 'sentFeedback', + ); + if (res) { + dispatchModalEvent(ModalEvent.FeedbackCreateModal)(); + } + + setFormBusy(false); + }, + [createMutation], + ); const onError = useCallback((err: typeof errors) => { Sentry.captureException(err); diff --git a/src/pages/api/feedback.ts b/src/pages/api/feedback.ts deleted file mode 100644 index 07a174120..000000000 --- a/src/pages/api/feedback.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import nextConnect from 'next-connect'; -import type { NextApiResponse } from 'next'; -import { getServerSession } from 'next-auth/next'; - -import { authOptions } from '../../utils/auth'; - -const route = nextConnect({ - onError(error: Error, _, res: NextApiResponse) { - res.status(500).json({ error: `Something went wrong: ${error.message}` }); - }, - onNoMatch: (_, res: NextApiResponse) => { - res.status(400).json({ error: 'We are sorry, but it is impossible' }); - }, -}); - -route.post(async (req: any, res: NextApiResponse) => { - if (!process.env.FEEDBACK_URL) { - return res.status(401).json({ error: 'Feedback url is not set' }); - } - - const { body: parsedBody } = req; - parsedBody.userAgent = req.headers['user-agent']; - const session = await getServerSession(req, res, authOptions); - - if (!session?.user) { - return res.status(403).json({ error: 'User is not authorized' }); - } - - const { name, email, image } = session.user; - parsedBody.name = name; - parsedBody.email = email; - parsedBody.avatarUrl = image; - - const feedbackResponse = await fetch(process.env.FEEDBACK_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(parsedBody), - }); - - return res.status(feedbackResponse.status).send(feedbackResponse.body); -}); - -export default route; diff --git a/src/schema/criteria.ts b/src/schema/criteria.ts index aa1659991..ea1e6b840 100644 --- a/src/schema/criteria.ts +++ b/src/schema/criteria.ts @@ -8,7 +8,11 @@ export const criteriaSchema = z.object({ required_error: tr('Title is required'), }) .min(1, { - message: tr('Title must be longer than 1 symbol'), + message: tr + .raw('Title must be longer than {upTo} symbol', { + upTo: 1, + }) + .join(''), }), weight: z.string().optional(), goalId: z.string(), diff --git a/src/schema/feedback.ts b/src/schema/feedback.ts index 93ef2a1e9..5a1b6ace2 100644 --- a/src/schema/feedback.ts +++ b/src/schema/feedback.ts @@ -9,9 +9,14 @@ export const createFeedbackSchema = z.object({ invalid_type_error: tr('Title must be a string'), }) .min(3, { - message: tr('Title must be longer than 3 symbol'), + message: tr + .raw('Title must be longer than {upTo} symbol', { + upTo: 3, + }) + .join(''), }), description: z.string().optional(), + href: z.string().optional(), }); export type CreateFeedback = z.infer; diff --git a/src/schema/filter.ts b/src/schema/filter.ts index f3d50b9cd..444ee9fc1 100644 --- a/src/schema/filter.ts +++ b/src/schema/filter.ts @@ -10,7 +10,11 @@ export const createFilterSchema = z.object({ invalid_type_error: tr('Title must be a string'), }) .min(1, { - message: tr('Title must be longer than 1 symbol'), + message: tr + .raw('Title must be longer than {upTo} symbol', { + upTo: 1, + }) + .join(''), }), mode: z.nativeEnum(FilterMode), params: z.string().min(1), diff --git a/src/utils/declareSsrProps.ts b/src/utils/declareSsrProps.ts index 53a1c6f7e..e13a69103 100644 --- a/src/utils/declareSsrProps.ts +++ b/src/utils/declareSsrProps.ts @@ -39,7 +39,14 @@ export function declareSsrProps( }; } - const ssrHelpers = createServerSideHelpers({ router: trpcRouter, ctx: { session }, transformer: superjson }); + const ssrHelpers = createServerSideHelpers({ + router: trpcRouter, + ctx: { + session, + headers: req.headers, + }, + transformer: superjson, + }); const ssrTime = Date.now(); diff --git a/trpc/context.ts b/trpc/context.ts index 10e9989ff..f50b4cc41 100644 --- a/trpc/context.ts +++ b/trpc/context.ts @@ -7,7 +7,7 @@ import { authOptions } from '../src/utils/auth'; export const createContext = async (opts: trpcNext.CreateNextContextOptions) => { const session = await getServerSession(opts.req, opts.res, authOptions); - return { session }; + return { session, headers: opts.req.headers }; }; export type TrpcContext = inferAsyncReturnType; diff --git a/trpc/router/feedback.ts b/trpc/router/feedback.ts new file mode 100644 index 000000000..b41de8673 --- /dev/null +++ b/trpc/router/feedback.ts @@ -0,0 +1,29 @@ +import { protectedProcedure, router } from '../trpcBackend'; +import { createFeedbackSchema } from '../../src/schema/feedback'; + +export const feedback = router({ + create: protectedProcedure + .input(createFeedbackSchema) + .mutation(async ({ ctx, input: { title, description, href } }) => { + if (!process.env.FEEDBACK_URL) { + return; + } + const { name, email, image } = ctx.session.user; + const userAgent = ctx.headers['user-agent']; + await fetch(process.env.FEEDBACK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title, + description, + href, + userAgent, + name, + email, + avatarUrl: image, + }), + }); + }), +}); diff --git a/trpc/router/index.ts b/trpc/router/index.ts index c4cd6023e..5803995e5 100644 --- a/trpc/router/index.ts +++ b/trpc/router/index.ts @@ -10,6 +10,7 @@ import { goal } from './goal'; import { search } from './search'; import { state } from './state'; import { estimates } from './estimates'; +import { feedback } from './feedback'; export const trpcRouter = router({ filter, @@ -22,6 +23,7 @@ export const trpcRouter = router({ search, state, estimates, + feedback, }); export type TrpcRouter = typeof trpcRouter; diff --git a/trpc/trpcBackend.ts b/trpc/trpcBackend.ts index 3ebe4b77b..44f6c2448 100644 --- a/trpc/trpcBackend.ts +++ b/trpc/trpcBackend.ts @@ -17,7 +17,7 @@ const sessionCheck = t.middleware(({ next, ctx }) => { } return next({ - ctx: { session }, + ctx: { session, headers: ctx.headers }, }); });