diff --git a/nextjs-13/.env.sample b/nextjs-13/.env.sample index 477c6642..992a33e6 100644 --- a/nextjs-13/.env.sample +++ b/nextjs-13/.env.sample @@ -4,7 +4,14 @@ OAUTH_GOOGLE_CLIENT_SECRET= NEXTAUTH_SECRET= NEXTAUTH_URL=http://localhost:3300 MAILGUN_DOMAIN= -MAILGUN_SMTP_HOST= -MAILGUN_SMTP_PORT= + +# set the credentials like this if you want to use mailgun +MAILGUN_SMTP_HOST=smtp.mailgun.org +MAILGUN_SMTP_PORT=587 + +# set the credentials like this if you want to use mailcatcher +MAILGUN_SMTP_HOST=0.0.0.0 +MAILGUN_SMTP_PORT=1025 + MAILGUN_SMTP_USERNAME= MAILGUN_SMTP_PASSWORD= diff --git a/nextjs-13/Makefile b/nextjs-13/Makefile new file mode 100644 index 00000000..9ce9bbcb --- /dev/null +++ b/nextjs-13/Makefile @@ -0,0 +1,11 @@ +.PHONY: dev migrate_db + +dev: + make migrate_db & + yarn dev & + redis-server & + yarn worker & + maildev + +migrate_db: + npx prisma migrate dev --name init diff --git a/nextjs-13/README.md b/nextjs-13/README.md index 81c92dbf..ceb7fea0 100644 --- a/nextjs-13/README.md +++ b/nextjs-13/README.md @@ -27,42 +27,13 @@ cp .env.sample .env yarn install ``` -2. Migrate database: +2. Run the development process: ```bash - prisma generate - - prisma migrate dev - ``` - -3. Run the development server: - - ```bash - npm run dev - # or - yarn dev - ``` - -4. Start maildev - - ```bash - maildev - # then go to http://0.0.0.0:1080/#/ - ``` - -5. Start redis + worker - - ```bash - # install redis if not already, port 6379 - redis-server + make dev ``` - ```bash - yarn worker - ``` - - -6. Open [http://localhost:3300](http://localhost:3300) with your browser to see the result. +3. Open [http://localhost:3300](http://localhost:3300) with your browser to see the result. ## Tests diff --git a/nextjs-13/package.json b/nextjs-13/package.json index c6bbf0bf..82e8cf9b 100644 --- a/nextjs-13/package.json +++ b/nextjs-13/package.json @@ -10,8 +10,7 @@ "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", - "workerr": "dotenv -e .env.local -- npx tsx --watch src/workers/sample.worker.ts", - "worker": "npx tsx --watch src/workers/sample.worker.ts" + "worker": "npx tsx --watch src/workers/email.worker.ts" }, "dependencies": { "@faker-js/faker": "^8.0.1", @@ -25,8 +24,8 @@ "@types/react": "18.2.6", "@types/react-dom": "18.2.4", "axios": "^1.4.0", - "classnames": "^2.3.2", "bullmq": "^3.15.2", + "classnames": "^2.3.2", "eslint": "8.40.0", "eslint-config-next": "13.4.3", "handlebars": "^4.7.7", diff --git a/nextjs-13/src/app/actions/actions.test.tsx b/nextjs-13/src/app/actions/actions.test.tsx index 18b830c0..aa4420a4 100644 --- a/nextjs-13/src/app/actions/actions.test.tsx +++ b/nextjs-13/src/app/actions/actions.test.tsx @@ -2,16 +2,16 @@ * @jest-environment node */ import { Prisma } from '@prisma/client'; +import { StatusCodes } from 'http-status-codes'; import { newsletterFactory } from '@test/factories/newsletter.factory'; import { userFactory } from '@test/factories/user.factory'; -import { invalidParamsMessage } from 'lib/request/getInvalidParamsError'; +import { errorMessageList } from 'lib/request/error'; import withAuth from 'lib/withAuth/withAuth'; import { createNewsletter as createRecord, updateNewsletter as updateRecord, deleteNewsletter as deleteRecord, - countNewsletters, } from 'repositories/newsletter.repository'; import { sendMailQueue } from 'workers/email.worker'; @@ -29,7 +29,7 @@ jest.mock('lib/withAuth/withAuth'); jest.mock('repositories/newsletter.repository'); jest.mock('workers/email.worker', () => ({ sendMailQueue: { - add: jest.fn(), + addBulk: jest.fn(), }, })); @@ -75,7 +75,7 @@ describe('createNewsletter', () => { throw new Prisma.PrismaClientValidationError(); }); await expect(createNewsletter(requestBody)).rejects.toThrow( - invalidParamsMessage + errorMessageList[StatusCodes.UNPROCESSABLE_ENTITY] ); expect(createRecord).toHaveBeenCalledWith({ @@ -193,23 +193,6 @@ describe('sendNewsletter', () => { }); }); - describe('given invalid newsletter ids', () => { - it('returns invalid newsletter error', async () => { - const user = { id: '1' }; - const requestBody = { - email: 'dev@nimblehq.co', - ids: ['1'], - }; - - withAuth.mockImplementation((callback) => callback(user)); - countNewsletters.mockResolvedValue(0); - - await expect(sendNewsletter(requestBody)).rejects.toThrow( - 'Invalid newsletters' - ); - }); - }); - describe('given valid newsletter ids', () => { it('triggers sendMail', async () => { const user = { id: '1', name: 'Dave' }; @@ -219,16 +202,20 @@ describe('sendNewsletter', () => { }; withAuth.mockImplementation((callback) => callback(user)); - countNewsletters.mockResolvedValue(requestBody.ids.length); await sendNewsletter(requestBody); - expect(sendMailQueue.add).toHaveBeenCalledWith('sendMail', { - ids: requestBody.ids, - to: requestBody.email, - senderId: user.id, - senderName: user.name, - }); + expect(sendMailQueue.addBulk).toHaveBeenCalledWith([ + { + data: { + id: requestBody.ids[0], + to: requestBody.email, + senderId: user.id, + senderName: user.name, + }, + name: 'sendMail', + }, + ]); }); }); }); diff --git a/nextjs-13/src/app/actions/actions.tsx b/nextjs-13/src/app/actions/actions.tsx index 435f9b1e..4f172e4d 100644 --- a/nextjs-13/src/app/actions/actions.tsx +++ b/nextjs-13/src/app/actions/actions.tsx @@ -1,11 +1,11 @@ -import RequestError from 'lib/request/error'; -import { invalidParamsMessage } from 'lib/request/getInvalidParamsError'; +import { StatusCodes } from 'http-status-codes'; + +import RequestError, { errorMessageList } from 'lib/request/error'; import withAuth from 'lib/withAuth/withAuth'; import { deleteNewsletter as deleteRecord, updateNewsletter as updateRecord, createNewsletter as createRecord, - countNewsletters as countRecords, } from 'repositories/newsletter.repository'; import { sendMailQueue } from 'workers/email.worker'; @@ -13,7 +13,7 @@ export async function deleteNewsletter(id: string) { return withAuth(async (currentUser) => { const result = await deleteRecord(id, currentUser.id); - if (result.count === 0) { + if (!result.count) { throw new RequestError({ message: 'Newsletter could not be deleted' }); } }); @@ -41,7 +41,7 @@ export async function updateNewsletter({ data, }); - if (result.count === 0) { + if (!result.count) { throw new RequestError({ message: 'Newsletter could not be updated' }); } }); @@ -64,7 +64,9 @@ export async function createNewsletter({ await createRecord(attributes); } catch (err) { - throw new RequestError({ message: invalidParamsMessage }); + throw new RequestError({ + message: errorMessageList[StatusCodes.UNPROCESSABLE_ENTITY], + }); } }); } @@ -89,21 +91,22 @@ export async function sendNewsletter({ throw new RequestError({ message: 'Invalid email' }); } - if (ids.length === 0) { - throw new RequestError({ message: 'Invalid newsletters' }); - } - - const newslettersCount = await countRecords(currentUser.id, ids); - const allIdsAreValid = newslettersCount === ids.length; - if (!allIdsAreValid) { + if (!ids.length) { throw new RequestError({ message: 'Invalid newsletters' }); } - sendMailQueue.add('sendMail', { - ids, - to: email, - senderId: currentUser.id, - senderName: currentUser.name, - }); + sendMailQueue.addBulk( + ids.map((id) => { + return { + name: 'sendMail', + data: { + id, + to: email, + senderId: currentUser.id, + senderName: currentUser.name, + }, + }; + }) + ); }); } diff --git a/nextjs-13/src/app/api/v1/newsletter/[id]/route.ts b/nextjs-13/src/app/api/v1/newsletter/[id]/route.ts index 9c36e29c..906c1e5a 100644 --- a/nextjs-13/src/app/api/v1/newsletter/[id]/route.ts +++ b/nextjs-13/src/app/api/v1/newsletter/[id]/route.ts @@ -1,7 +1,7 @@ import { StatusCodes } from 'http-status-codes'; import { NextResponse, NextRequest } from 'next/server'; -import getInvalidParamsError from 'lib/request/getInvalidParamsError'; +import { invalidParamsResponseError } from 'lib/request/error'; import { findNewsletter } from 'repositories/newsletter.repository'; export async function GET( @@ -19,6 +19,6 @@ export async function GET( return NextResponse.json({ record }, { status: StatusCodes.OK }); } catch (err) { - return getInvalidParamsError(); + return invalidParamsResponseError(); } } diff --git a/nextjs-13/src/app/api/v1/newsletter/route.test.ts b/nextjs-13/src/app/api/v1/newsletter/route.test.ts index a700a9ff..6673e0ee 100644 --- a/nextjs-13/src/app/api/v1/newsletter/route.test.ts +++ b/nextjs-13/src/app/api/v1/newsletter/route.test.ts @@ -6,7 +6,7 @@ import { StatusCodes } from 'http-status-codes'; import { newsletterFactory } from '@test/factories/newsletter.factory'; import appHandler from 'lib/handler/app.handler'; -import { queryNewsletters } from 'repositories/newsletter.repository'; +import { queryNewsletterList } from 'repositories/newsletter.repository'; import { GET } from './route'; @@ -19,12 +19,12 @@ describe('GET /v1/newsletter', () => { const newsletterAttributes = { id: '1', userId: user.id }; const newsletter = { ...newsletterFactory, ...newsletterAttributes }; appHandler.mockImplementation((req, callback) => callback(user, {})); - queryNewsletters.mockResolvedValue([newsletter]); + queryNewsletterList.mockResolvedValue([newsletter]); const response = await GET({}); const responseBody = await response.json(); - expect(queryNewsletters).toHaveBeenCalledWith(user.id); + expect(queryNewsletterList).toHaveBeenCalledWith(user.id); expect(response.status).toBe(StatusCodes.OK); expect(responseBody.records[0]).toMatchObject({ id: newsletterAttributes.id, diff --git a/nextjs-13/src/app/api/v1/newsletter/route.ts b/nextjs-13/src/app/api/v1/newsletter/route.ts index 21b13065..f3a787ed 100644 --- a/nextjs-13/src/app/api/v1/newsletter/route.ts +++ b/nextjs-13/src/app/api/v1/newsletter/route.ts @@ -2,11 +2,11 @@ import { StatusCodes } from 'http-status-codes'; import { NextResponse, NextRequest } from 'next/server'; import appHandler from 'lib/handler/app.handler'; -import { queryNewsletters } from 'repositories/newsletter.repository'; +import { queryNewsletterList } from 'repositories/newsletter.repository'; export async function GET(req: NextRequest) { return appHandler(req, async (currentUser, _) => { - const records = await queryNewsletters(currentUser.id); + const records = await queryNewsletterList(currentUser.id); return NextResponse.json({ records: records }, { status: StatusCodes.OK }); }); diff --git a/nextjs-13/src/app/api/v1/newsletter/send/route.test.ts b/nextjs-13/src/app/api/v1/newsletter/send/route.test.ts new file mode 100644 index 00000000..af906d51 --- /dev/null +++ b/nextjs-13/src/app/api/v1/newsletter/send/route.test.ts @@ -0,0 +1,103 @@ +/** + * @jest-environment node + */ +import { StatusCodes } from 'http-status-codes'; + +import appHandler from 'lib/handler/app.handler'; +import { sendMailQueue } from 'workers/email.worker'; + +import { POST } from './route'; + +jest.mock('lib/handler/app.handler'); +jest.mock('repositories/newsletter.repository'); +jest.mock('workers/email.worker', () => ({ + sendMailQueue: { + addBulk: jest.fn(), + }, +})); + +describe('POST /v1/newsletter/send', () => { + describe('given invalid email', () => { + it('returns invalid email error', async () => { + const user = { id: '1' }; + const requestBody = { + json: () => { + return { + email: 'devnimblehq.co', + ids: ['1'], + }; + }, + }; + + appHandler.mockImplementation((_, callback) => + callback(user, requestBody) + ); + + const response = await POST(requestBody); + const responseBody = await response.json(); + + expect(response.status).toBe(StatusCodes.UNPROCESSABLE_ENTITY); + expect(responseBody).toMatchObject({ + message: 'Invalid email', + }); + }); + + describe('given valid email', () => { + describe('given empty newsletter ids', () => { + it('returns invalid newsletter error', async () => { + const user = { id: '1' }; + const requestBody = { + json: () => { + return { + email: 'dev@nimblehq.co', + ids: [], + }; + }, + }; + + appHandler.mockImplementation((_, callback) => + callback(user, requestBody) + ); + + const response = await POST(requestBody); + const responseBody = await response.json(); + + expect(response.status).toBe(StatusCodes.UNPROCESSABLE_ENTITY); + expect(responseBody).toMatchObject({ + message: 'Invalid newsletters', + }); + }); + }); + + describe('given valid newsletter ids', () => { + it('triggers sendMail', async () => { + const user = { id: '1', name: 'Dave' }; + const data = { + email: 'dev@nimblehq.co', + ids: ['1'], + }; + const requestBody = { json: () => data }; + + appHandler.mockImplementation((_, callback) => + callback(user, requestBody) + ); + + const response = await POST(requestBody); + + expect(response.status).toBe(StatusCodes.OK); + expect(sendMailQueue.addBulk).toHaveBeenCalledWith([ + { + data: { + id: data.ids[0], + to: data.email, + senderId: user.id, + senderName: user.name, + }, + name: 'sendMail', + }, + ]); + }); + }); + }); + }); +}); diff --git a/nextjs-13/src/app/api/v1/newsletter/send/route.ts b/nextjs-13/src/app/api/v1/newsletter/send/route.ts new file mode 100644 index 00000000..f702f672 --- /dev/null +++ b/nextjs-13/src/app/api/v1/newsletter/send/route.ts @@ -0,0 +1,56 @@ +import { StatusCodes } from 'http-status-codes'; +import { NextResponse, NextRequest } from 'next/server'; + +import appHandler from 'lib/handler/app.handler'; +import { + invalidParamsResponseError, + badRequestResponse, +} from 'lib/request/error'; +import { sendMailQueue } from 'workers/email.worker'; + +const validateEmail = (email) => { + return String(email) + .toLowerCase() + .match( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ); +}; + +export async function POST(req: NextRequest) { + return appHandler(req, async (currentUser) => { + try { + const body = await req.json(); + const email = body.email; + + if (!validateEmail(email)) { + return badRequestResponse('Invalid email'); + } + + const ids = [...new Set(body.ids)]; + if (!ids.length) { + return badRequestResponse('Invalid newsletters'); + } + + sendMailQueue.addBulk( + ids.map((id) => { + return { + name: 'sendMail', + data: { + id, + to: email, + senderId: currentUser.id, + senderName: currentUser.name, + }, + }; + }) + ); + + return NextResponse.json( + { status: 'success' }, + { status: StatusCodes.OK } + ); + } catch (err) { + return invalidParamsResponseError(); + } + }); +} diff --git a/nextjs-13/src/app/auth/sign-in/page.test.tsx b/nextjs-13/src/app/auth/sign-in/page.test.tsx index 2af03695..e694a91b 100644 --- a/nextjs-13/src/app/auth/sign-in/page.test.tsx +++ b/nextjs-13/src/app/auth/sign-in/page.test.tsx @@ -5,10 +5,9 @@ import { signIn } from 'next-auth/react'; import SignInPage from './page'; jest.mock('next-auth/react'); -jest.mock('next/navigation'); describe('SignInPage', () => { - it('renders h4', () => { + it('renders the title', () => { render(); expect(screen.getByText('NextNewsletter 🚀')).toBeInTheDocument(); diff --git a/nextjs-13/src/app/newsletter/[id]/page.test.tsx b/nextjs-13/src/app/newsletter/[id]/page.test.tsx index e26653bd..0d6eaa47 100644 --- a/nextjs-13/src/app/newsletter/[id]/page.test.tsx +++ b/nextjs-13/src/app/newsletter/[id]/page.test.tsx @@ -29,7 +29,7 @@ describe('ViewNewsletter', () => { }); }); - describe('giving fetching data', () => { + describe('given fetching data', () => { beforeEach(() => { requestManager.mockImplementation(() => new Promise(() => [])); }); @@ -47,7 +47,7 @@ describe('ViewNewsletter', () => { }); }); - describe('giving NOT fetching data', () => { + describe('given NOT fetching data', () => { it('does NOT render ClipLoader', async () => { render(); diff --git a/nextjs-13/src/app/newsletter/[id]/page.tsx b/nextjs-13/src/app/newsletter/[id]/page.tsx index 228e11db..73fc4f75 100644 --- a/nextjs-13/src/app/newsletter/[id]/page.tsx +++ b/nextjs-13/src/app/newsletter/[id]/page.tsx @@ -9,43 +9,42 @@ import NewsletterDetail from '@components/NewsletterDetail'; import requestManager from 'lib/request/manager'; const ViewNewsletter = () => { - const [newsletter, setNewsletter] = useState(); - const [isLoading, setIsLoading] = useState(false); + const [record, setRecord] = useState([]); const [hasError, setHasError] = useState(false); + const [isLoading, setIsLoading] = useState(false); const params = useParams(); const id = params.id; - useEffect(() => { - const fetchRecord = async () => { - setIsLoading(true); + const getNewletter = async () => { + setIsLoading(true); - try { - const response = await requestManager('GET', `v1/newsletter/${id}`); + try { + const response = await requestManager('GET', `v1/newsletter/${id}`); - setIsLoading(false); - setNewsletter(response.record); - } catch (error) { - setHasError(true); - } - }; + setIsLoading(false); + setRecord(response.record); + } catch (error) { + setHasError(true); + setIsLoading(false); + } + }; - fetchRecord(); + useEffect(() => { + getNewletter(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (hasError) { - redirect('/'); - } + if (hasError) redirect('/auth/sign-in'); }, [hasError]); return (
- {isLoading || !newsletter ? ( - + {isLoading ? ( + ) : ( - + )}
); diff --git a/nextjs-13/src/app/page.test.tsx b/nextjs-13/src/app/page.test.tsx index cb8d6b3c..8dd37a5d 100644 --- a/nextjs-13/src/app/page.test.tsx +++ b/nextjs-13/src/app/page.test.tsx @@ -8,7 +8,6 @@ import requestManager from 'lib/request/manager'; import Home from './page'; jest.mock('next-auth/react'); -jest.mock('next/navigation'); jest.mock('lib/request/manager'); jest.mock('react-spinners', () => ({ @@ -18,11 +17,7 @@ jest.mock('@components/NewsletterModal', () => { return jest.fn(() =>
); }); jest.mock('@components/ListNewsletter', () => { - return jest.fn((props) => ( -
- {props.promise ? props.promise.read() : []} -
- )); + return jest.fn(() =>
); }); describe('Home', () => { @@ -38,7 +33,7 @@ describe('Home', () => { }); }); - describe('giving fetching data', () => { + describe('given fetching data', () => { beforeEach(() => { requestManager.mockImplementation(() => new Promise(() => [])); }); @@ -56,7 +51,7 @@ describe('Home', () => { }); }); - describe('giving NOT fetching data', () => { + describe('given NOT fetching data', () => { it('does NOT render ClipLoader', async () => { render(); @@ -69,7 +64,7 @@ describe('Home', () => { render(); await waitFor(() => { - expect(screen.getByTestId('list-newsletter')).toBeInTheDocument(); + expect(screen.getByTestId('list-newsletter')).toBeVisible(); }); }); }); diff --git a/nextjs-13/src/app/page.tsx b/nextjs-13/src/app/page.tsx index 50c10310..d337582d 100644 --- a/nextjs-13/src/app/page.tsx +++ b/nextjs-13/src/app/page.tsx @@ -18,7 +18,7 @@ const Home = () => { const [records, setRecords] = useState([]); const [isLoading, setIsLoading] = useState(false); - const getData = async () => { + const getNewletters = async () => { setIsLoading(true); try { @@ -33,7 +33,7 @@ const Home = () => { }; useEffect(() => { - getData(); + getNewletters(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -63,7 +63,7 @@ const Home = () => { ) : ( )} @@ -75,7 +75,7 @@ const Home = () => { diff --git a/nextjs-13/src/app/send/page.test.tsx b/nextjs-13/src/app/send/page.test.tsx index 245f4dcd..7a44ad1e 100644 --- a/nextjs-13/src/app/send/page.test.tsx +++ b/nextjs-13/src/app/send/page.test.tsx @@ -2,10 +2,10 @@ import React, { useState } from 'react'; import { toast } from 'react-toastify'; import { render, waitFor, screen, fireEvent } from '@testing-library/react'; +import { StatusCodes } from 'http-status-codes'; import { sendNewsletter } from 'app/actions/newsletter'; -import RequestError from 'lib/request/error'; -import { invalidParamsMessage } from 'lib/request/getInvalidParamsError'; +import RequestError, { errorMessageList } from 'lib/request/error'; import requestManager from 'lib/request/manager'; import SendNewsletter from './page'; @@ -14,6 +14,7 @@ jest.mock('lib/request/getServerSession', () => ({ getServerSession: jest.fn(), })); jest.mock('lib/request/manager'); + jest.mock('react-toastify', () => ({ toast: { success: jest.fn(), @@ -109,7 +110,7 @@ describe('SendNewsletter', () => { await waitFor(() => { expect(toast.error).toHaveBeenCalledWith( - 'Please select atleast one newsletter', + 'Please select at least one newsletter', { position: 'top-center', autoClose: 3000, @@ -124,17 +125,17 @@ describe('SendNewsletter', () => { // eslint-disable-next-line react-hooks/rules-of-hooks return useState([{ value: 1 }]); }; - jest.spyOn(React, 'useState').mockImplementationOnce(stateMock); const mockRequest = (_) => { return { records: [] }; }; - requestManager.mockImplementation(mockRequest); sendNewsletter.mockImplementation(() => { - throw new RequestError({ message: invalidParamsMessage }); + throw new RequestError({ + message: errorMessageList[StatusCodes.UNPROCESSABLE_ENTITY], + }); }); render(); @@ -146,11 +147,14 @@ describe('SendNewsletter', () => { fireEvent.submit(sendButton); await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith(invalidParamsMessage, { - position: 'top-center', - autoClose: 3000, - hideProgressBar: false, - }); + expect(toast.error).toHaveBeenCalledWith( + errorMessageList[StatusCodes.UNPROCESSABLE_ENTITY], + { + position: 'top-center', + autoClose: 3000, + hideProgressBar: false, + } + ); }); }); }); diff --git a/nextjs-13/src/app/send/page.tsx b/nextjs-13/src/app/send/page.tsx index ff908b64..ff86077d 100644 --- a/nextjs-13/src/app/send/page.tsx +++ b/nextjs-13/src/app/send/page.tsx @@ -15,7 +15,7 @@ const SendNewsletter = () => { const [email, setEmail] = useState(''); const startTransition = useTransition()[1]; - const getData = async () => { + const getNewletters = async () => { setIsLoading(true); try { @@ -30,22 +30,21 @@ const SendNewsletter = () => { }; useEffect(() => { - getData(); - // eslint-disable-next-line react-hooks/exhaustive-deps + getNewletters(); }, []); const afterSubmit = () => { setIsLoading(false); setSelected([]); setEmail(''); - getData(); + getNewletters(); }; - const handleSubmit = async (event) => { + const resetState = async (event) => { event.preventDefault(); - if (selected.length === 0) { - return makeToast('Please select atleast one newsletter', 'error'); + if (!selected.length) { + return makeToast('Please select at least one newsletter', 'error'); } startTransition(async () => { @@ -69,8 +68,8 @@ const SendNewsletter = () => { {isLoading ? ( ) : ( -
-