-
Notifications
You must be signed in to change notification settings - Fork 1
Alex/implement email notification functionality #464
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
base: develop
Are you sure you want to change the base?
Changes from 55 commits
66835c2
9049bfa
5f3d9e5
ac4c293
13ec1c6
d4cfc88
a0eafb5
9bc34ef
906e83f
e880373
9205f10
f50fe0e
b7685f7
621497e
85e48dd
0ce258f
4f2709f
1cb0041
7c7f7f2
76ee015
7b53303
a1ee17e
27737c3
c0932e2
415bbcc
7ffb7fd
32f507b
854634c
75e0bde
771d6aa
f147c19
0c216bb
8d41257
3864f3a
584de00
af8287b
8c86b6e
1fd8fe8
1903d27
4e44876
17e7057
75b5b4e
3624dc0
cafd345
b97e185
97f3ae8
368f41e
0d8d397
9605af5
6246c4f
6a58f10
8e3148a
9c864d5
a0a2c1c
e85408c
b4f7874
b12ea64
3e2aadd
247a786
1188d47
e55c39f
8eb3bd6
f4e006c
0641f59
aa4c65d
5119a15
1717433
d207ea0
7bf57bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| 'use server'; | ||
| import { messaging } from "@/api/serverConfig"; | ||
| import { sendEmailNotifications } from "./sendEmailNotification"; | ||
|
|
||
| jest.mock('./sendEmailNotification', () => ({ | ||
| sendEmailNotifications: jest.fn(), | ||
| })) | ||
|
|
||
| jest.mock('@/api/serverConfig', () => ({ | ||
| messaging: { | ||
| createEmail: jest.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| describe('SendEmailNotification', () => { | ||
| it('should send email with provided information', async () => { | ||
| const content = 'Test'; | ||
| const sendEmailUsers = ['123456', '12345', '1234']; | ||
| const subject = 'This is a test'; | ||
|
choir241 marked this conversation as resolved.
Outdated
|
||
|
|
||
| (sendEmailNotifications as jest.Mock).mockImplementation(async ({content, sendEmailUsers, subject, testBCC}) => { | ||
| await (messaging.createEmail as jest.Mock)('1234567890', subject, content, [], sendEmailUsers, [], [], []); | ||
| }) | ||
|
|
||
| await sendEmailNotifications({content, sendEmailUsers, subject}); | ||
|
|
||
| expect(messaging.createEmail).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| subject, | ||
| content, | ||
| [], | ||
|
choir241 marked this conversation as resolved.
Outdated
|
||
| sendEmailUsers, | ||
| [], | ||
| [], | ||
| [], | ||
| ); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Copyright (c) Gridiron Survivor. | ||
| // Licensed under the MIT License. | ||
|
|
||
| 'use server'; | ||
| import { ID } from 'appwrite'; | ||
| import { messaging } from '@/api/serverConfig'; | ||
|
|
||
| /** | ||
| * Function to send email. | ||
| * @param props - subject, content. | ||
| * @param props.content - The actual email you are wanting to send. | ||
| * @param props.groupUsers - User id's being passed in from the notification page. | ||
| * @param props.subject - The subject of the email. | ||
| */ | ||
| export const sendEmailNotifications = async ({ | ||
| content, | ||
| groupUsers, | ||
| subject, | ||
| }: { | ||
| content: string; | ||
| groupUsers: string[]; | ||
| subject: string; | ||
| }): Promise<void> => { | ||
| try { | ||
| await messaging.createEmail( | ||
| ID.unique(), | ||
| subject, | ||
| content, | ||
| [], | ||
| groupUsers, | ||
| [], | ||
| [], | ||
| [], | ||
| ); | ||
|
choir241 marked this conversation as resolved.
Outdated
|
||
| } catch (error) { | ||
| throw new Error('Error Sending Email'); | ||
|
choir241 marked this conversation as resolved.
Outdated
|
||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,69 @@ | ||
| // /Users/ryanfurrer/Developer/GitHub/gridiron-survivor/app/(admin)/admin/notifications/page.test.tsx | ||
|
|
||
| import { render, screen } from '@testing-library/react'; | ||
| import { fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
| import { getCurrentLeague } from '@/api/apiFunctions'; | ||
| import { sendEmailNotifications } from './actions/sendEmailNotification'; | ||
| import AdminNotifications from './page'; | ||
| import React from 'react'; | ||
|
|
||
| let contentInput: HTMLInputElement, | ||
| selectAllUsersRadioOption: HTMLElement, | ||
| selectRecipientsRadioGroup: HTMLElement, | ||
| sendEmailButton: HTMLElement, | ||
| subjectInput: HTMLInputElement; | ||
|
|
||
| jest.mock('@/api/apiFunctions', () => ({ | ||
| getCurrentLeague: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('./actions/sendEmailNotification', () => ({ | ||
| sendEmailNotifications: jest.fn(), | ||
| })); | ||
|
|
||
| describe('Admin notifications page', () => { | ||
| it(`should render it's content`, () => { | ||
| beforeEach(async () => { | ||
| jest.clearAllMocks(); | ||
|
|
||
| (getCurrentLeague as jest.Mock).mockResolvedValue({ | ||
| participants: ['12345', '1234', '123'], | ||
| leagueName: 'Test League', | ||
| }); | ||
|
|
||
| render(<AdminNotifications />); | ||
|
|
||
| contentInput = screen.getByTestId('content-text'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update this if you take my previous suggestions into account. |
||
| selectAllUsersRadioOption = screen.getByTestId('all-users-option'); | ||
| selectRecipientsRadioGroup = screen.getByTestId('radio-group-default'); | ||
| sendEmailButton = screen.getByTestId('send-email'); | ||
| subjectInput = screen.getByTestId('subject-text'); | ||
| }); | ||
|
|
||
| it(`should render it's content`, () => { | ||
|
choir241 marked this conversation as resolved.
|
||
| (sendEmailNotifications as jest.Mock).mockResolvedValue({}); | ||
|
|
||
| const adminNotificationsContent = screen.getByTestId( | ||
| 'admin-notifications-content', | ||
| ); | ||
| expect(adminNotificationsContent).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should call the sendEmailNotifications function with the provided inputs', async () => { | ||
| fireEvent.click(selectAllUsersRadioOption); | ||
| fireEvent.change(subjectInput, { target: { value: 'Test Title' } }); | ||
| fireEvent.change(contentInput, { | ||
| target: { value: 'Test message section.' }, | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(sendEmailButton).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| fireEvent.submit(sendEmailButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(sendEmailNotifications as jest.Mock).toHaveBeenCalledWith({ | ||
| content: 'Test message section.', | ||
| groupUsers: ['12345', '1234', '123'], | ||
| subject: 'Test Title', | ||
|
choir241 marked this conversation as resolved.
|
||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,160 @@ | ||
| // Copyright (c) Gridiron Survivor. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { JSX } from 'react'; | ||
| 'use client'; | ||
| import { Button } from '@/components/Button/Button'; | ||
| import { getCurrentLeague } from '@/api/apiFunctions'; | ||
| import { Input } from '@/components/Input/Input'; | ||
| import { JSX, useState } from 'react'; | ||
| import { LabelText } from '@/components/LabelText/LabelText'; | ||
| import { | ||
| RadioGroupDefault, | ||
| RadioGroupDefaultItem, | ||
| } from '@/components/RadioGroupDefault/RadioGroupDefault'; | ||
| import { sendEmailNotifications } from './actions/sendEmailNotification'; | ||
| import { Textarea } from '@/components/Textarea/Textarea'; | ||
| import React, { useEffect } from 'react'; | ||
|
|
||
| /** | ||
| * The admin home page. | ||
| * @returns The rendered AdminHome page. | ||
| */ | ||
| const AdminNotifications = (): JSX.Element => { | ||
| const [content, setContent] = useState<string>(''); | ||
| const [emailSubjects, setEmailSubjects] = useState<string>('all users'); | ||
| const [groupUsers, setGroupUsers] = useState<string[]>([]); | ||
| const [leagueName, setLeagueName] = useState<string>(''); | ||
| const [subject, setSubject] = useState<string>(''); | ||
|
|
||
| /** | ||
| * To grab all users from the league. | ||
| * @returns The league data. | ||
| */ | ||
|
choir241 marked this conversation as resolved.
|
||
| const getLeagueData = async (): Promise<void> => { | ||
| try { | ||
| const leagueId = '66e1cc9000160b10bf2c'; // TEST LEAGUE (DO NOT JOIN) | ||
| const leagueData = await getCurrentLeague(leagueId); | ||
| setGroupUsers(leagueData.participants); | ||
| setLeagueName(leagueData.leagueName); | ||
| } catch (error) { | ||
| console.error('Error Sending Email:', error); | ||
|
choir241 marked this conversation as resolved.
Outdated
|
||
| throw new Error('Error Sending Email'); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Handle form submission | ||
| * @param event - Prevents the default reloading. | ||
| */ | ||
| const handleSubmit = async (event: React.FormEvent): Promise<void> => { | ||
| event.preventDefault(); | ||
| await sendEmailNotifications({ | ||
| content, | ||
| groupUsers, | ||
| subject, | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * Function to handle radio selection logic. | ||
| * @param value - Value of the radio buttons. | ||
| */ | ||
| const handleRadioChange = (value: string): void => { | ||
| setGroupUsers([value]); | ||
| setEmailSubjects(value); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| /** | ||
| * Fetches the league data. | ||
| * @returns The league data. | ||
| */ | ||
| const fetchData = async (): Promise<void> => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be in a useEfect or can it just be called when the component mounts? |
||
| await getLeagueData(); | ||
| }; | ||
| fetchData(); | ||
| }, []); | ||
|
|
||
| return ( | ||
| <section data-testid="admin-notifications-content"> | ||
| <p>{`This is where I'd put my notifation dashboard, IF I HAD ONE!`}</p> | ||
| <section | ||
| className="flex flex-col space-y-6" | ||
| data-testid="admin-notifications-content" | ||
| > | ||
| <p> | ||
| Choose the users you would like to email in{' '} | ||
| <span className="font-bold text-orange-500">{leagueName}</span>. | ||
| </p> | ||
| <RadioGroupDefault | ||
| defaultValue="all users" | ||
| onValueChange={handleRadioChange} | ||
| required | ||
| > | ||
| <div className="flex items-center space-x-2"> | ||
| <RadioGroupDefaultItem | ||
| value="all users" | ||
| id="all" | ||
| data-testid="all-users-option" | ||
| /> | ||
| <LabelText htmlFor="all">All users</LabelText> | ||
| </div> | ||
| <div className="flex items-center space-x-2"> | ||
| <RadioGroupDefaultItem | ||
| value="all survivors" | ||
| id="survivors" | ||
| data-testid="only-survivors-option" | ||
| /> | ||
| <LabelText htmlFor="survivors">Only the survivors</LabelText> | ||
| </div> | ||
| <div className="flex items-center space-x-2"> | ||
| <RadioGroupDefaultItem | ||
| value="all losers" | ||
| id="losers" | ||
| data-testid="only-losers-option" | ||
| /> | ||
| <LabelText htmlFor="losers">Only the losers</LabelText> | ||
| </div> | ||
| </RadioGroupDefault> | ||
| <form | ||
| onSubmit={handleSubmit} | ||
| className="flex flex-col space-y-6 max-w-[80ch]" | ||
| > | ||
| <div className="flex gap-2 flex-col"> | ||
| <LabelText htmlFor="subject" className="text-lg"> | ||
| Subject: | ||
| </LabelText> | ||
| <Input | ||
| data-testid="subject-text" | ||
| id="subject" | ||
| name="subject" | ||
| onChange={(e) => setSubject(e.target.value)} | ||
| type="text" | ||
| /> | ||
| </div> | ||
| <div className="flex gap-2 flex-col"> | ||
| <LabelText htmlFor="content" className="text-lg"> | ||
| Message: | ||
| </LabelText> | ||
| <Textarea | ||
| data-testid="content-text" | ||
| id="content" | ||
| name="content" | ||
| onChange={(e) => setContent(e.target.value)} | ||
| /> | ||
| </div> | ||
| <p> | ||
| This email will be sent to{' '} | ||
| <span className="font-bold text-orange-500"> | ||
| {emailSubjects.toLowerCase()} | ||
| </span>{' '} | ||
| in <span className="font-bold text-orange-500">{leagueName}</span> | ||
| </p> | ||
| <Button | ||
| className="md:max-w-fit" | ||
| data-testid="send-email" | ||
| label="Send email" | ||
| type="submit" | ||
| /> | ||
| </form> | ||
| </section> | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { LabelText } from './LabelText'; | ||
| import { render } from '@testing-library/react'; | ||
| import React from 'react'; | ||
|
|
||
| describe('LabelText Component', () => { | ||
| it('renders properly', () => { | ||
| const { getByTestId } = render(<LabelText>Test Label</LabelText>); | ||
| expect(getByTestId('label-text')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders the correct text', () => { | ||
| const { getByTestId } = render(<LabelText>Test Label</LabelText>); | ||
| expect(getByTestId('label-text')).toHaveTextContent('Test Label'); | ||
| }); | ||
|
|
||
| it('applies disabled styles when peer-disabled', () => { | ||
| const { getByTestId } = render( | ||
| <LabelText className="peer-disabled">Test Label</LabelText>, | ||
| ); | ||
| expect(getByTestId('label-text')).toHaveClass( | ||
| 'peer-disabled:cursor-not-allowed', | ||
| ); | ||
| expect(getByTestId('label-text')).toHaveClass('peer-disabled:opacity-70'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // Copyright (c) Gridiron Survivor. | ||
| // Licensed under the MIT License. | ||
|
|
||
| "use client" | ||
| import { cn } from "@/utils/utils" | ||
| import { cva, type VariantProps } from "class-variance-authority" | ||
| import * as LabelPrimitive from "@radix-ui/react-label" | ||
| import * as React from "react" | ||
|
|
||
| const labelTextVariants = cva( | ||
| "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" | ||
| ) | ||
|
|
||
| const LabelText = React.forwardRef< | ||
| React.ElementRef<typeof LabelPrimitive.Root>, | ||
| React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & | ||
| VariantProps<typeof labelTextVariants> | ||
| >(({ className, ...props }, ref) => ( | ||
| <LabelPrimitive.Root | ||
| ref={ref} | ||
| className={cn(labelTextVariants(), className)} | ||
| data-testid="label-text" | ||
| {...props} | ||
| /> | ||
| )) | ||
| LabelText.displayName = LabelPrimitive.Root.displayName | ||
|
|
||
| export { LabelText } |
Uh oh!
There was an error while loading. Please reload this page.