From ea152f21d2641ce2b33860a3b81e0e4028e57cac Mon Sep 17 00:00:00 2001 From: Parichay Barpanda Date: Thu, 5 Aug 2021 14:32:05 +0530 Subject: [PATCH] Partial feedback impl --- components/FeedbackForm/FeedbackForm.tsx | 72 +++++++++++++++++++ components/FeedbackForm/FeedbackTimeline.tsx | 31 ++++++++ .../FeedbackForm/feedbackform.module.css | 46 ++++++++++++ components/Footer/Footer.tsx | 8 +-- lib/constants.ts | 1 + lib/feedback/add.ts | 43 +++++++++++ lib/feedback/getFeedback.ts | 50 +++++++++++++ pages/api/feedback/addFeedback.ts | 19 +++++ pages/api/feedback/feedbacks.ts | 19 +++++ pages/feedback.tsx | 23 ++++++ styles/pageStyles/feedback.module.css | 5 ++ 11 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 components/FeedbackForm/FeedbackForm.tsx create mode 100644 components/FeedbackForm/FeedbackTimeline.tsx create mode 100644 components/FeedbackForm/feedbackform.module.css create mode 100644 lib/feedback/add.ts create mode 100644 lib/feedback/getFeedback.ts create mode 100644 pages/api/feedback/addFeedback.ts create mode 100644 pages/api/feedback/feedbacks.ts create mode 100644 pages/feedback.tsx create mode 100644 styles/pageStyles/feedback.module.css diff --git a/components/FeedbackForm/FeedbackForm.tsx b/components/FeedbackForm/FeedbackForm.tsx new file mode 100644 index 0000000..6dfc7bf --- /dev/null +++ b/components/FeedbackForm/FeedbackForm.tsx @@ -0,0 +1,72 @@ +import cn from 'classnames'; +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useForm } from 'react-hook-form'; +import ButtonLoading from '../ButtonLoading/ButtonLoading'; +import styles from './feedbackform.module.css'; +import useMutation from '../../hooks/useMutation'; +import { IAddFeedbackResponse } from '../../lib/feedback/add'; +import constants from '../../lib/constants'; +import FeedbackTimeline from './FeedbackTimeline'; + +const AddFeedbackSchema = yup.object().shape({ + content: yup + .string() + .required('Add some feedback') + .min(100, 'Add atleast 100 characters'), +}); + +export interface IAddFeedbackFieldValues { + content: string; +} + +export default function FeedbackForm() { + const { + handleSubmit, + register, + formState: { errors }, + // reset, + } = useForm({ + resolver: yupResolver(AddFeedbackSchema), + }); + + const { loading, mutate } = useMutation< + IAddFeedbackFieldValues, + IAddFeedbackResponse + >(constants.feedbackAddApiRoute, (res) => { + // eslint-disable-next-line no-console + console.log(res); + }); + + const addFeedback = (data: IAddFeedbackFieldValues) => { + mutate(data); + }; + + return ( + <> +
+
+ + {errors.content && ( +

{errors.content.message}

+ )} +
+
+ +
+
+ + + ); +} diff --git a/components/FeedbackForm/FeedbackTimeline.tsx b/components/FeedbackForm/FeedbackTimeline.tsx new file mode 100644 index 0000000..c179508 --- /dev/null +++ b/components/FeedbackForm/FeedbackTimeline.tsx @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import { fetcher } from '../../lib/apiUtils'; + +export interface IFeedbackResponse { + email: string; + content: string; + createdAt: string; + updatedAt: string; + id: string; +} + +export default function FeedbackTimeline() { + const [next, setNext] = useState(true); + const [feedbacks, setFeedbacks] = useState>([]); + + useEffect(() => { + if (next) { + fetcher<{}, any>('/api/feedback/feedbacks?p=0').then((res) => { + setFeedbacks((prev) => prev.concat(res.feedbacks)), setNext(false); + }); + } + }, [next]); + + return ( + <> + {feedbacks.map((feedback) => ( +
{feedback.content}
+ ))} + + ); +} diff --git a/components/FeedbackForm/feedbackform.module.css b/components/FeedbackForm/feedbackform.module.css new file mode 100644 index 0000000..7012113 --- /dev/null +++ b/components/FeedbackForm/feedbackform.module.css @@ -0,0 +1,46 @@ +.form_styles { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.input_box { + width: 100%; + max-width: 500px; +} + +.input_box input { + width: 100%; + border: 1px solid var(--button-hover-color); + padding: 1rem; + height: 100px; +} + +.input_box button { + width: 100%; + border: 1px solid var(--button-hover-color); + padding: 1rem; + background-color: var(--button-color); + color: var(--default-bw); + font-size: var(--h4-font-size); + cursor: pointer; + margin-top: var(--mb-1); + height: 52px; /* Hard coding height to match height with text during loading */ +} + +.button_disable button { + background-color: var(--button-color-disabled); + cursor: no-drop; +} + +.error_message { + margin-top: var(--mb-1); + color: var(--error-color); +} + +.error_message::before { + display: inline; + content: '⚠ '; +} diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx index e060c41..059b1e8 100644 --- a/components/Footer/Footer.tsx +++ b/components/Footer/Footer.tsx @@ -34,11 +34,11 @@ export default function Footer() { Blog - {/*
  • - - About +
  • + + Feedback -
  • */} +
      {/*
    • diff --git a/lib/constants.ts b/lib/constants.ts index 257cb21..e9feebc 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -5,6 +5,7 @@ const constants = { newsletterUnsubscribeApiRoute: '/api/newsletter/unsubscribe', newsletterUpdateApiRoute: '/api/newsletter/update', newsletterUpdateServerSideRoute: '/subscription/update', + feedbackAddApiRoute: '/api/feedback/addFeedback', verifyEmailTemplatePath: '/emailTemplates/emailconfirmation.pug', postsPath: 'content/posts', }; diff --git a/lib/feedback/add.ts b/lib/feedback/add.ts new file mode 100644 index 0000000..90c69b7 --- /dev/null +++ b/lib/feedback/add.ts @@ -0,0 +1,43 @@ +import firebase from '../../firebase/clientApp'; +import * as Sentry from '@sentry/nextjs'; +import { IGenericAPIResponse } from '../apiUtils'; + +export interface IAddFeedbackResponse extends IGenericAPIResponse {} + +export default async function add({ + content, +}: { + content: string; +}): Promise { + const db = firebase.firestore(); + try { + const result = await db + .collection('feedback') + .add({ + content: content, + userId: 'xyz', + createdAt: firebase.firestore.Timestamp.fromDate(new Date()), + updatedAt: firebase.firestore.Timestamp.fromDate(new Date()), + }) + .then((docRef) => { + return { + error: false, + message: 'Feedback has been added. ' + docRef.id, + }; + }) + .catch((error) => { + Sentry.captureException(error); + return { + error: true, + message: 'Feedback could not be added. ' + error.message, + }; + }); + return result; + } catch (error) { + Sentry.captureException(error); + return { + error: true, + message: 'Some error occurred: ' + error.message, + }; + } +} diff --git a/lib/feedback/getFeedback.ts b/lib/feedback/getFeedback.ts new file mode 100644 index 0000000..b065590 --- /dev/null +++ b/lib/feedback/getFeedback.ts @@ -0,0 +1,50 @@ +import * as Sentry from '@sentry/nextjs'; +import firebase from '../../firebase/clientApp'; + +// export interface IFeedbacksResponse extends IFeedbackResponse { +// error: boolean; +// message: string; +// } + +export default async function getFeedback({ + page, +}: { + page: number; +}): Promise { + const db = firebase.firestore(); + try { + const result = await db + .collection('feedback') + .orderBy('createdAt') + .startAt(page) + .limit(10) + .get() + .then((querySnapshot) => { + if (querySnapshot.docs.length === 0) { + null; + } + return { + error: false, + message: 'Recevied feedbacks!', + data: querySnapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })), + }; + }) + .catch((error) => { + Sentry.captureException(error); + return { + error: true, + message: 'Error fetching feedback: ' + error.message, + }; + }); + return result; + } catch (error) { + Sentry.captureException(error); + return { + error: true, + message: 'Some error occurred: ' + error.message, + }; + } +} diff --git a/pages/api/feedback/addFeedback.ts b/pages/api/feedback/addFeedback.ts new file mode 100644 index 0000000..2555c19 --- /dev/null +++ b/pages/api/feedback/addFeedback.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import add from '../../../lib/feedback/add'; +import { rateLimiterMiddleWare } from '../../../lib/rateLimiter'; + +export default async function resend( + req: NextApiRequest, + res: NextApiResponse +) { + const rateLimitRes = await rateLimiterMiddleWare(req, res); + if (rateLimitRes.error) { + return res.status(429).json({ error: true, message: rateLimitRes.message }); + } + const content = req.body.content; + const result = await add({ content }); + if (result.error) { + return res.status(200).json({ error: true, message: result.message }); + } + res.status(200).json({ error: false, message: result.message }); +} diff --git a/pages/api/feedback/feedbacks.ts b/pages/api/feedback/feedbacks.ts new file mode 100644 index 0000000..a90d50f --- /dev/null +++ b/pages/api/feedback/feedbacks.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import getFeedback from '../../../lib/feedback/getFeedback'; +import { rateLimiterMiddleWare } from '../../../lib/rateLimiter'; + +export default async function resend( + req: NextApiRequest, + res: NextApiResponse +) { + const rateLimitRes = await rateLimiterMiddleWare(req, res); + if (rateLimitRes.error) { + return res.status(429).json({ error: true, message: rateLimitRes.message }); + } + const page = parseInt(req.query.p as string, 10); + const result = await getFeedback({ page }); + if (result.error) { + return res.status(200).json({ error: true, message: result.message }); + } + res.status(200).json({ error: false, feedbacks: result.data }); +} diff --git a/pages/feedback.tsx b/pages/feedback.tsx new file mode 100644 index 0000000..7c2fd5d --- /dev/null +++ b/pages/feedback.tsx @@ -0,0 +1,23 @@ +import RootLayout from '../layouts/RootLayout'; +import rootStyles from '../styles/root.module.css'; +import cn from 'classnames'; +import FeedbackForm from '../components/FeedbackForm/FeedbackForm'; +import feedbackStyles from '../styles/pageStyles/feedback.module.css'; + +export default function feedback() { + return ( + +
      +
      + +
      +
      +
      + ); +} diff --git a/styles/pageStyles/feedback.module.css b/styles/pageStyles/feedback.module.css new file mode 100644 index 0000000..1e4293a --- /dev/null +++ b/styles/pageStyles/feedback.module.css @@ -0,0 +1,5 @@ +.container { + gap: 2rem; + max-width: var(--big-screen-width); + margin: 4rem auto; +}