From 22b44746cf0028d55f6dcc4aa7f34b3ce4049226 Mon Sep 17 00:00:00 2001 From: Parker Date: Tue, 18 Jul 2023 17:53:32 -0700 Subject: [PATCH 01/23] responsive navbar --- app/root.tsx | 32 ++++++++++++++++++-------------- tailwind.config.ts | 3 +++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 3c1d1c9..ee685d9 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -160,15 +160,19 @@ function App() { ) if (user) { nav = ( -
- - {userIsAdmin ? : null} - +
+
+ + {userIsAdmin ? : null} +
+
+ +
) } @@ -184,8 +188,8 @@ function App() {
-
+ + ) +} + +async function getRecipientsFromRoles(roles: string[]) { + const recipients = new Set() + if (roles.includes('allVolunteers')) { + const users = await prisma.user.findMany() + users.map(user => user.email).forEach(email => recipients.add(email)) + } else { + for (let role of roles) { + const users = await prisma.user.findMany({ + where: { roles: { some: { name: role } } }, + }) + users.map(user => user.email).forEach(email => recipients.add(email)) + } + } + return recipients +} From c6f32eb4b21a47b12e4c0907f1dfad6b1e588a6f Mon Sep 17 00:00:00 2001 From: Parker Date: Wed, 6 Dec 2023 14:18:24 -0700 Subject: [PATCH 07/23] Remove ability to email multiple people --- app/utils/email.server.ts | 34 +++++++++++++++++++--------------- tests/mocks/utils.ts | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/utils/email.server.ts b/app/utils/email.server.ts index 007071e..6df9156 100644 --- a/app/utils/email.server.ts +++ b/app/utils/email.server.ts @@ -1,17 +1,21 @@ import { type ReactElement } from 'react' import { renderAsync } from '@react-email/components' import { siteEmailAddressWithName } from '~/data.ts' -import { Resend } from 'resend'; +import { Resend } from 'resend' -const resend = new Resend(process.env.RESEND_API_KEY); +const resend = new Resend(process.env.RESEND_API_KEY) export async function sendEmail({ react, ...options }: { - to: string | string[] + to: string subject: string - attachments?: { filename?: string, path?: string, content?: string | Buffer }[] + attachments?: { + filename?: string + path?: string + content?: string | Buffer + }[] } & ( | { html: string; text: string; react?: never } | { react: ReactElement; html?: never; text?: never } @@ -20,7 +24,7 @@ export async function sendEmail({ const from = siteEmailAddressWithName const email = { - from, + from, ...options, ...(react ? await renderReactEmail(react) : null), } @@ -57,7 +61,7 @@ export async function sendEmail({ html: email.html, text: email.text, attachments: email.attachments, - }); + }) return { status: 'success', @@ -65,16 +69,16 @@ export async function sendEmail({ } as const // Catch full exception - } catch (e : any) { - console.error('🔴 error sending email:', JSON.stringify(email)) + } catch (e: any) { + console.error('🔴 error sending email:', JSON.stringify(email)) return { - status: 'error', - error: { - message: e.message || 'Unknown error', - code: e.code || 'Unknown code', - response: e.response || 'Unknown response', - } - } + status: 'error', + error: { + message: e.message || 'Unknown error', + code: e.code || 'Unknown code', + response: e.response || 'Unknown response', + }, + } } } diff --git a/tests/mocks/utils.ts b/tests/mocks/utils.ts index d749800..113ef1c 100644 --- a/tests/mocks/utils.ts +++ b/tests/mocks/utils.ts @@ -21,7 +21,7 @@ export async function createFixture( } export const emailSchema = z.object({ - to: z.union([z.string(), z.array(z.string())]), + to: z.string(), from: z.string(), subject: z.string(), text: z.string(), From b421ff64fcc57090d7220652bfabf5a4dbe7dfa4 Mon Sep 17 00:00:00 2001 From: Parker Date: Wed, 6 Dec 2023 14:25:20 -0700 Subject: [PATCH 08/23] Remove xsm --- tailwind.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/tailwind.config.ts b/tailwind.config.ts index b8473a7..a6a070b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -15,9 +15,6 @@ export default { }, }, extend: { - screens: { - 'xsm': '400px', - }, colors: { border: 'hsl(var(--color-border))', input: { From c78472b3dac079dea25a2a789c443db7e0c7d617 Mon Sep 17 00:00:00 2001 From: Parker Date: Thu, 7 Dec 2023 10:08:03 -0700 Subject: [PATCH 09/23] Add email to admin menu, lightly format email --- app/components/ui/icon.svg | 1 + app/components/ui/icon.tsx | 43 ++++++++++--------- app/data.ts | 4 +- app/root.tsx | 9 +++- .../admin+/_email+/CustomEmail.server.tsx | 23 ++++++++++ app/routes/admin+/_email+/email.tsx | 21 +++++---- other/svg-icons/email.svg | 1 + 7 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 app/routes/admin+/_email+/CustomEmail.server.tsx create mode 100644 other/svg-icons/email.svg diff --git a/app/components/ui/icon.svg b/app/components/ui/icon.svg index 47596bc..8dc5cc5 100644 --- a/app/components/ui/icon.svg +++ b/app/components/ui/icon.svg @@ -72,6 +72,7 @@ /> + ' +export const siteEmailAddressWithName = + siteName + ' ' +export const siteBaseUrl = 'https://thebarn.trottrack.org' export const volunteerTypes = [ { diff --git a/app/root.tsx b/app/root.tsx index 3c1d1c9..6e3242d 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -281,7 +281,7 @@ function AdminDropdown() { return ( - + + Submit + @@ -211,5 +242,5 @@ async function getRecipientsFromRoles(roles: string[]) { users.map(user => user.email).forEach(email => recipients.add(email)) } } - return recipients + return Array.from(recipients) } diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index 01ccfd0..cf06fa4 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -188,7 +188,7 @@ export default function EditUserProfile() { username: data.user.username, name: data.user.name ?? '', email: data.user.email, - mailingList: data.user.mailingList, + mailingList: data.user.mailingList ? 'on' : undefined, phone: data.user.phone, birthdate: formattedBirthdate ?? '', height: data.user.height ?? '', diff --git a/app/utils/zod-extensions.ts b/app/utils/zod-extensions.ts index f2a1f68..f6d38ee 100644 --- a/app/utils/zod-extensions.ts +++ b/app/utils/zod-extensions.ts @@ -10,21 +10,23 @@ export const checkboxSchema = (msgWhenRequired?: string) => { : transformedValue } -export const optionalDateSchema = z.preprocess(arg => { +export const optionalDateSchema = z.preprocess(arg => { if (typeof arg !== 'string') { return undefined } if (arg != '') { return new Date(arg) } - return undefined + return undefined }, z.date().optional()) export const optionalDateTimeZoneSchema = z.union([ - z.string() + z + .string() .transform(date => date + 'T00:00:00-07:00') .pipe(z.coerce.date()), - z.string() + z + .string() .nullish() .transform(date => null), -]) \ No newline at end of file +]) From ef0465a0c91f6e9f5c7398426b7c42651ed46a48 Mon Sep 17 00:00:00 2001 From: Parker Date: Fri, 8 Dec 2023 08:57:21 -0700 Subject: [PATCH 12/23] Add check for mailingList opt-in before emailing --- app/routes/admin+/_email+/email.tsx | 59 +++++++++++++++++++++-------- app/routes/settings+/profile.tsx | 16 ++++---- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/app/routes/admin+/_email+/email.tsx b/app/routes/admin+/_email+/email.tsx index 1d7aca6..8565294 100644 --- a/app/routes/admin+/_email+/email.tsx +++ b/app/routes/admin+/_email+/email.tsx @@ -31,7 +31,6 @@ const emailFormSchema = z lessonAssistant: checkboxSchema(), horseLeader: checkboxSchema(), instructor: checkboxSchema(), - admin: checkboxSchema(), subject: z .string() .min(1, { message: 'Your email must include a subject' }), @@ -53,7 +52,9 @@ export async function action({ request, params }: DataFunctionArgs) { const formData = await request.formData() const submission = parse(formData, { schema: emailFormSchema }) if (!submission.value) { - return json({ status: 'error', submission } as const, { status: 400 }) + return json({ status: 'error', submission, error: 'error' } as const, { + status: 400, + }) } // Get list of people to email const roles = [ @@ -61,11 +62,22 @@ export async function action({ request, params }: DataFunctionArgs) { 'lessonAssistant', 'horseLeader', 'instructor', - 'admin', ] const selectedRoles = roles.filter(role => submission.payload[role] === 'on') const recipients = await getRecipientsFromRoles(selectedRoles) + if (recipients.length === 0) { + return json( + { + status: 'error', + error: 'no-recipients', + submission, + recipients, + } as const, + { status: 400 }, + ) + } + for (let recipient of recipients) { sendEmail({ to: recipient, @@ -77,7 +89,9 @@ export async function action({ request, params }: DataFunctionArgs) { 'There was an error sending emails', JSON.stringify(result.error), ) - return json({ status: 'error', result } as const, { status: 400 }) + return json({ status: 'error', error: 'error', result } as const, { + status: 400, + }) } }) } @@ -85,6 +99,7 @@ export async function action({ request, params }: DataFunctionArgs) { { status: 'success', submission, + error: null, recipients, } as const, { status: 200 }, @@ -118,6 +133,13 @@ export default function Email() { recipients.length } recipient${plural ? 's' : ''}`, }) + } else if (actionData?.error === 'no-recipients') { + toast({ + variant: 'destructive', + title: 'No recipients', + description: + 'There are no users with that role that are accepting emails', + }) } else { toast({ variant: 'destructive', @@ -179,16 +201,6 @@ export default function Email() { }} errors={fields.instructor.errors} /> -
@@ -233,13 +245,28 @@ async function getRecipientsFromRoles(roles: string[]) { const recipients = new Set() if (roles.includes('allVolunteers')) { const users = await prisma.user.findMany() - users.map(user => user.email).forEach(email => recipients.add(email)) + users + .filter(user => user.mailingList) + .map(user => user.email) + .forEach(email => recipients.add(email)) } else { for (let role of roles) { const users = await prisma.user.findMany({ where: { roles: { some: { name: role } } }, }) - users.map(user => user.email).forEach(email => recipients.add(email)) + users + .filter(user => user.mailingList) + .map(user => user.email) + .forEach(email => recipients.add(email)) + + // Include admin on all emails + const admin = await prisma.user.findMany({ + where: { roles: { some: { name: 'admin' } } }, + }) + admin + .filter(user => user.mailingList) + .map(user => user.email) + .forEach(email => recipients.add(email)) } } return Array.from(recipients) diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index cf06fa4..43685dc 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -241,7 +241,7 @@ export default function EditUserProfile() { Account Change password -
+
Date: Fri, 8 Dec 2023 09:25:18 -0700 Subject: [PATCH 13/23] Revert some changes, remove email opt-in from test --- app/root.tsx | 4 ++-- tests/e2e/onboarding.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 4c6c231..f49f402 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -249,9 +249,9 @@ function UserDropdown() { - + - Account + Profile diff --git a/tests/e2e/onboarding.test.ts b/tests/e2e/onboarding.test.ts index aab80e0..ac50f1e 100644 --- a/tests/e2e/onboarding.test.ts +++ b/tests/e2e/onboarding.test.ts @@ -52,7 +52,7 @@ test('onboarding with link', async ({ page }) => { const secretTextbox = page.getByRole('textbox', { name: /secret/i }) await secretTextbox.click() - await secretTextbox.fill("horses are cool") + await secretTextbox.fill('horses are cool') await page.getByRole('button', { name: /submit/i }).click() await expect( @@ -76,7 +76,9 @@ test('onboarding with link', async ({ page }) => { await page.getByRole('textbox', { name: /^name/i }).fill(onboardingData.name) - await page.getByRole('textbox', { name: /number/i}).fill(onboardingData.phone) + await page + .getByRole('textbox', { name: /number/i }) + .fill(onboardingData.phone) await page.getByLabel(/^password/i).fill(onboardingData.password) @@ -84,8 +86,6 @@ test('onboarding with link', async ({ page }) => { await page.getByLabel(/terms/i).check() - await page.getByLabel(/opportunities to volunteer/i).check() - await page.getByLabel(/remember me/i).check() await page.getByRole('button', { name: /Create an account/i }).click() @@ -128,8 +128,8 @@ test('onboarding with a short code', async ({ page }) => { const secretTextbox = page.getByRole('textbox', { name: /secret/i }) await secretTextbox.click() - await secretTextbox.fill("horses are cool") - + await secretTextbox.fill('horses are cool') + await page.getByRole('button', { name: /submit/i }).click() await expect( page.getByRole('button', { name: /submit/i, disabled: true }), From b1af2d6c6a534574dcacee9c3458eecff591dc48 Mon Sep 17 00:00:00 2001 From: Parker Date: Fri, 8 Dec 2023 09:36:19 -0700 Subject: [PATCH 14/23] Combat flaky test --- tests/e2e/2fa.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/2fa.test.ts b/tests/e2e/2fa.test.ts index 02f76a8..9516b7f 100644 --- a/tests/e2e/2fa.test.ts +++ b/tests/e2e/2fa.test.ts @@ -39,6 +39,7 @@ test('Users can add 2FA to their account and use it when logging in', async ({ await page.getByRole('link', { name: user.name ?? user.username }).click() await page.getByRole('menuitem', { name: /logout/i }).click() + await page.goto('/login') // added to fix persistent flaky behavior await page.goto('/login') await expect(page).toHaveURL(`/login`) await page.getByRole('textbox', { name: /username/i }).fill(user.username) From 7ea7ac26059d5180adb0bbd7491c77bdf971cca0 Mon Sep 17 00:00:00 2001 From: Greg V Date: Fri, 22 Dec 2023 12:19:49 -0700 Subject: [PATCH 15/23] Admin to allow for seeing mailingList --- .../admin+/_users+/users.edit.$userId.tsx | 29 ++++++++++++++++--- app/routes/admin+/_users+/users.tsx | 7 +++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/routes/admin+/_users+/users.edit.$userId.tsx b/app/routes/admin+/_users+/users.edit.$userId.tsx index 83a9ee5..dfc145e 100644 --- a/app/routes/admin+/_users+/users.edit.$userId.tsx +++ b/app/routes/admin+/_users+/users.edit.$userId.tsx @@ -41,7 +41,8 @@ import { format } from 'date-fns' const editUserSchema = z.object({ name: nameSchema.optional(), username: usernameSchema, - email: emailSchema.optional(), + email: emailSchema.optional(), + mailingList: checkboxSchema(), phone: phoneSchema, birthdate: optionalDateSchema, height: z.coerce.number().min(0).optional(), @@ -93,6 +94,7 @@ export async function action({ request, params }: DataFunctionArgs) { username, birthdate, phone, + mailingList, height, yearsOfExperience, isInstructor, @@ -125,6 +127,7 @@ export async function action({ request, params }: DataFunctionArgs) { name, username, phone, + mailingList : mailingList ?? false, birthdate: birthdate ?? null, height: height ?? null, yearsOfExperience: yearsOfExperience ?? null, @@ -184,6 +187,7 @@ export default function EditUser() { name: data.user?.name ?? '', username: data.user?.username ?? '', email: data.user?.email, + mailingList: data.user?.mailingList ?? false, phone: data.user?.phone, birthdate: formattedBirthdate ?? '', height: data.user?.height ?? '', @@ -247,7 +251,7 @@ export default function EditUser() { disabled: true, }} errors={fields.email.errors} - /> + /> -
+
+ +
+
+ +
+ />
diff --git a/app/routes/admin+/_users+/users.tsx b/app/routes/admin+/_users+/users.tsx index 4837e60..336b990 100644 --- a/app/routes/admin+/_users+/users.tsx +++ b/app/routes/admin+/_users+/users.tsx @@ -45,6 +45,13 @@ export const columns: ColumnDef[] = [ accessorKey: 'email', header: 'email', }, + { + header: 'mailing list', + accessorFn: (row) => { + const isMailingList = row.mailingList + return isMailingList ? 'Yes' : 'No' + }, + }, { accessorKey: 'name', header: 'name', From d2c7bcf15a0c1e58ec21178336744b18e3a85f53 Mon Sep 17 00:00:00 2001 From: Greg V Date: Fri, 22 Dec 2023 12:20:56 -0700 Subject: [PATCH 16/23] Cal updates 1/ Move button to top to create 2/ Tooltip to see what is available 3/ Center on mobile and desktop now are full width --- app/routes/calendar+/index.tsx | 35 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/app/routes/calendar+/index.tsx b/app/routes/calendar+/index.tsx index f728c18..499b43b 100644 --- a/app/routes/calendar+/index.tsx +++ b/app/routes/calendar+/index.tsx @@ -259,7 +259,7 @@ export async function action({ request }: ActionArgs) { export default function Schedule() { const data = useLoaderData() - const events = data.events + var events = data.events const horses = data.horses const instructors = data.instructors const user = useUser() @@ -269,6 +269,13 @@ export default function Schedule() { const [filterFlag, setFilterFlag] = useState(false) + // Modify event.title to include the count of volunteers registered + events.forEach(event => { + // event.tooltop is a string that should include how many volunteers are registered for each role and how many are needed + + event.tooltip = `Cleaning Crew: ${event.cleaningCrew.length} / ${event.cleaningCrewReq}\nSidewalkers: ${event.sideWalkers.length} / ${event.sideWalkersReq}\nLesson Assistants: ${event.lessonAssistants.length} / ${event.lessonAssistantsReq}\nHorse Leaders: ${event.horseLeaders.length} / ${event.horseLeadersReq}` + }) + const eventsThatNeedHelp = events.filter((event: (typeof events)[number]) => { return ( event.cleaningCrewReq > event.cleaningCrew.length || @@ -285,8 +292,8 @@ export default function Schedule() { return (
-

Calendar

-
+

Calendar

+
setFilterFlag(!filterFlag)} @@ -296,16 +303,22 @@ export default function Schedule() { Show only events that need more volunteers
-
+ + {userIsAdmin ? ( + + ) : null} + +
event.tooltip} startAccessor="start" endAccessor="end" onSelectEvent={handleSelectEvent} - style={{ - height: '100%', - width: '100%', + style={{ + height: '95%', + width: '95%', backgroundColor: 'white', color: 'black', padding: 20, @@ -313,7 +326,7 @@ export default function Schedule() { }} />
- + - {userIsAdmin ? ( - - ) : null} +
) } @@ -552,7 +563,7 @@ function CreateEventDialog({ horses, instructors }: CreateEventDialogProps) { return ( -