Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feedback button wrapper with API #1266

Merged
merged 1 commit into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Feedback title": "",
"Feedback description. Say anything what's on your mind": "",
"Send feedback": "",
"Cancel": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import ru from './ru.json';
import en from './en.json';

export type I18nKey = keyof typeof ru & keyof typeof en;
type I18nLang = 'ru' | 'en';

const keyset: I18nLangSet<I18nKey> = {};

keyset['ru'] = ru;
keyset['en'] = en;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Feedback title": "Заголовок заявки",
"Feedback description. Say anything what's on your mind": "Описание заявки. Расскажите что произошло",
"Send feedback": "Отправить заявку",
"Cancel": "Отменить"
}
86 changes: 86 additions & 0 deletions src/components/FeedbackCreateForm/FeedbackCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Form, FormActions, FormAction, FormTextarea, FormInput, ModalContent } from '@taskany/bricks';
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';

Check warning on line 10 in src/components/FeedbackCreateForm/FeedbackCreateForm.tsx

View workflow job for this annotation

GitHub Actions / build

'dispatchErrorNotification' is defined but never used
import { notifyPromise } from '../../utils/notifyPromise';

import { tr } from './FeedbackCreateForm.i18n';

const FeedbackCreateForm: React.FC = () => {
const [formBusy, setFormBusy] = useState(false);

const {
register,
handleSubmit,
formState: { errors, isSubmitted },
} = useForm<CreateFeedback>({
resolver: zodResolver(createFeedbackSchema),
});

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({
title: form.title,
description: form.description,
href: window.location.href,
}),
}),
Troff8 marked this conversation as resolved.
Show resolved Hide resolved
'sentFeedback',
);
if (res) {
dispatchModalEvent(ModalEvent.FeedbackCreateModal)();
}
setFormBusy(false);
}, []);

const onError = useCallback((err: typeof errors) => {
Sentry.captureException(err);
}, []);

return (
<ModalContent>
<Form disabled={formBusy} onSubmit={handleSubmit(onPending, onError)}>
<FormInput
{...register('title')}
placeholder={tr('Feedback title')}
flat="bottom"
brick="right"
error={errorsResolver('title')}
/>

<FormTextarea
{...register('description')}
minHeight={100}
placeholder={tr("Feedback description. Say anything what's on your mind")}
flat="both"
error={errorsResolver('description')}
/>

<FormActions flat="top">
<FormAction left inline />
<FormAction right inline>
<Button outline text={'Cancel'} onClick={dispatchModalEvent(ModalEvent.FeedbackCreateModal)} />
<Button view="primary" outline type="submit" text={tr('Send feedback')} />
</FormAction>
</FormActions>
</Form>
</ModalContent>
);
};

export default FeedbackCreateForm;
7 changes: 6 additions & 1 deletion src/components/NotificationsHub/NotificationHub.map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type Namespaces =
| SubscribeNamespacesAction<'goals'>
| 'userSettingsUpdate'
| 'tagCreate'
| 'userInvite';
| 'userInvite'
| 'sentFeedback';

export type { Namespaces as NotificationNamespaces };

Expand Down Expand Up @@ -129,6 +130,10 @@ export const notificationKeyMap: NotificationMap = {
success: tr('Voila! Successfully updated 🎉'),
pending: tr('We are updating user settings'),
},
sentFeedback: {
success: tr('Feedback sent 🎉'),
pending: tr('Feedback is formed'),
},
clearLSCache: tr('Local cache cleared successfully'),
error: tr('Something went wrong 😿'),
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
"We are creating something new": "We are creating something new",
"Voila! It's here 🎉": "Voila! It is here 🎉",
"We are updating project settings": "We are updating project settings",
"So sad! Project will miss you": "So sad! Project will miss you"
"So sad! Project will miss you": "So sad! Project will miss you",
"Feedback sent 🎉": "Feedback sent 🎉",
"Feedback is formed": "Feedback is formed"
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
"We are creating something new": "Погоди немного, создаем кое-что новое...",
"Voila! It's here 🎉": "Ура! Всё готово 🎉",
"We are updating project settings": "Обновляем настройки",
"So sad! Project will miss you": "Как жаль! Проект будет скучать по тебе..."
"So sad! Project will miss you": "Как жаль! Проект будет скучать по тебе...",
"Feedback sent 🎉": "Отзыв отправлен 🎉",
"Feedback is formed": "Отзыв формируется"
}
5 changes: 5 additions & 0 deletions src/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const GoalCreateForm = dynamic(() => import('./GoalCreateForm/GoalCreateForm'));
const UserInviteForm = dynamic(() => import('./UserInviteForm/UserInviteForm'));
const HotkeysModal = dynamic(() => import('./HotkeysModal/HotkeysModal'));
const NotificationsHub = dynamic(() => import('./NotificationsHub/NotificationsHub'));
const FeedbackCreateForm = dynamic(() => import('./FeedbackCreateForm/FeedbackCreateForm'));

interface PageProps {
user: Session['user'];
Expand Down Expand Up @@ -100,6 +101,10 @@ export const Page: React.FC<PageProps> = ({ user, ssrTime, title = 'Untitled', c
<UserInviteForm />
</ModalOnEvent>

<ModalOnEvent event={ModalEvent.FeedbackCreateModal}>
<FeedbackCreateForm />
</ModalOnEvent>

<HotkeysModal />

<NotificationsHub />
Expand Down
1 change: 1 addition & 0 deletions src/components/PageFooter/PageFooter.i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Feedback": "Feedback",
"Terms": "Terms",
"Docs": "Docs",
"Contact Taskany": "Contact Taskany",
Expand Down
1 change: 1 addition & 0 deletions src/components/PageFooter/PageFooter.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Feedback": "Обратная связь",
"Terms": "Условия",
"Docs": "Документация",
"Contact Taskany": "Контакты",
Expand Down
13 changes: 10 additions & 3 deletions src/components/PageFooter/PageFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FC } from 'react';
import { Footer, Link } from '@taskany/bricks';
import { FooterItem } from '@taskany/bricks/components/Footer';
import { Footer, FooterItem } from '@taskany/bricks/components/Footer';
import { gray9 } from '@taskany/colors';
import { Link } from '@taskany/bricks';

import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal';

import { tr } from './PageFooter.i18n';

Expand All @@ -13,9 +15,14 @@ export const PageFooter: FC = () => {
{ title: tr('API'), url: '/api' },
{ title: tr('About'), url: '/about' },
];

return (
<Footer>
<Link inline>
<FooterItem color={gray9} onClick={dispatchModalEvent(ModalEvent.FeedbackCreateModal)}>
{tr('Feedback')}
</FooterItem>
</Link>

{menuItems.map(({ title, url }) => (
<Link key={url} href={url} inline>
<FooterItem color={gray9}>{title}</FooterItem>
Expand Down
46 changes: 46 additions & 0 deletions src/pages/api/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* 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;
17 changes: 17 additions & 0 deletions src/schema/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod';

import { tr } from './schema.i18n';

export const createFeedbackSchema = z.object({
title: z
.string({
required_error: tr('Title is required'),
invalid_type_error: tr('Title must be a string'),
})
.min(3, {
message: tr('Title must be longer than 3 symbol'),
}),
description: z.string().optional(),
});

export type CreateFeedback = z.infer<typeof createFeedbackSchema>;
1 change: 1 addition & 0 deletions src/schema/schema.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"Title is required": "",
"Title must be a string": "",
"Title must be longer than 1 symbol": "",
"Title must be longer than 3 symbol": "",
"Comments's description is required": "",
"Comments's description must be a string": "",
"Comments's description must be longer than 1 symbol": "",
Expand Down
1 change: 1 addition & 0 deletions src/schema/schema.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"Title is required": "",
"Title must be a string": "",
"Title must be longer than 1 symbol": "",
"Title must be longer than 3 symbol": "",
"Comments's description is required": "",
"Comments's description must be a string": "",
"Comments's description must be longer than 1 symbol": "",
Expand Down
2 changes: 2 additions & 0 deletions src/utils/dispatchModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ModalEvent {
UserInviteModal = 'UserInviteModal',
FilterCreateModal = 'FilterCreateModal',
FilterDeleteModal = 'FilterDeleteModal',
FeedbackCreateModal = 'FeedbackCreateModal',
}

export interface MapModalToComponentProps {
Expand All @@ -25,6 +26,7 @@ export interface MapModalToComponentProps {
[ModalEvent.UserInviteModal]: unknown;
[ModalEvent.FilterCreateModal]: unknown;
[ModalEvent.FilterDeleteModal]: unknown;
[ModalEvent.FeedbackCreateModal]: unknown;
}

interface DispatchModalEvent {
Expand Down
Loading