Skip to content
Open
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
72 changes: 72 additions & 0 deletions components/FeedbackForm/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -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<IAddFeedbackFieldValues>({
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 (
<>
<form onSubmit={handleSubmit(addFeedback)} className={styles.form_styles}>
<div className={styles.input_box}>
<input
name="content"
type="text"
placeholder="Feedback"
{...register('content')}
/>
{errors.content && (
<p className={styles.error_message}>{errors.content.message}</p>
)}
</div>
<div
className={cn(styles.input_box, {
[styles.button_disable]: loading,
})}
>
<button type="submit" disabled={loading}>
{loading ? <ButtonLoading /> : 'Add Feedback'}
</button>
</div>
</form>
<FeedbackTimeline />
</>
);
}
31 changes: 31 additions & 0 deletions components/FeedbackForm/FeedbackTimeline.tsx
Original file line number Diff line number Diff line change
@@ -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<Array<IFeedbackResponse>>([]);

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) => (
<div key={feedback.id}>{feedback.content}</div>
))}
</>
);
}
46 changes: 46 additions & 0 deletions components/FeedbackForm/feedbackform.module.css
Original file line number Diff line number Diff line change
@@ -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: '⚠ ';
}
8 changes: 4 additions & 4 deletions components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export default function Footer() {
<a className={footerStyles.footer__link}>Blog</a>
</Link>
</li>
{/* <li>
<Link href="/about">
<a className={footerStyles.footer__link}>About</a>
<li>
<Link href="/feedback">
<a className={footerStyles.footer__link}>Feedback</a>
</Link>
</li> */}
</li>
</ul>
<ul className={footerStyles.footer__links_2}>
{/* <li>
Expand Down
1 change: 1 addition & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
Expand Down
43 changes: 43 additions & 0 deletions lib/feedback/add.ts
Original file line number Diff line number Diff line change
@@ -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<IAddFeedbackResponse> {
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,
};
}
}
50 changes: 50 additions & 0 deletions lib/feedback/getFeedback.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
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,
};
}
}
19 changes: 19 additions & 0 deletions pages/api/feedback/addFeedback.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
19 changes: 19 additions & 0 deletions pages/api/feedback/feedbacks.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
23 changes: 23 additions & 0 deletions pages/feedback.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<RootLayout>
<section className={rootStyles.section}>
<div
className={cn(
rootStyles.container,
rootStyles.grid,
feedbackStyles.container
)}
>
<FeedbackForm />
</div>
</section>
</RootLayout>
);
}
5 changes: 5 additions & 0 deletions styles/pageStyles/feedback.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.container {
gap: 2rem;
max-width: var(--big-screen-width);
margin: 4rem auto;
}