diff --git a/apps/member-profile/app/routes/_profile.profile.emails.tsx b/apps/member-profile/app/routes/_profile.profile.emails.tsx index ecf4e8c4..8b0f38b4 100644 --- a/apps/member-profile/app/routes/_profile.profile.emails.tsx +++ b/apps/member-profile/app/routes/_profile.profile.emails.tsx @@ -1,8 +1,24 @@ -import { json, LoaderFunctionArgs } from '@remix-run/node'; -import { Outlet, useLoaderData, useNavigate } from '@remix-run/react'; +import { ActionFunctionArgs, LoaderFunctionArgs, json } from '@remix-run/node'; +import { + Outlet, + Form as RemixForm, + useActionData, + useLoaderData, + useNavigate, + useSubmit, +} from '@remix-run/react'; import { Edit, Plus } from 'react-feather'; +import { z } from 'zod'; -import { Button, cx, Text } from '@oyster/ui'; +import { + Button, + Checkbox, + Form, + Text, + cx, + getActionErrors, + validateForm, +} from '@oyster/ui'; import { ProfileDescription, @@ -10,17 +26,27 @@ import { ProfileSection, ProfileTitle, } from '../shared/components/profile'; + import { Route } from '../shared/constants'; +import { db, updateAllowEmailShare } from '../shared/core.server'; import { track } from '../shared/mixpanel.server'; -import { listEmails } from '../shared/queries'; -import { ensureUserAuthenticated, user } from '../shared/session.server'; +import { getMember, listEmails } from '../shared/queries'; +import { + commitSession, + ensureUserAuthenticated, + toast, + user, +} from '../shared/session.server'; export async function loader({ request }: LoaderFunctionArgs) { const session = await ensureUserAuthenticated(request); const id = user(session); - const emails = await listEmails(id); + const [emails, student] = await Promise.all([ + listEmails(id), + getMember(id).select('allowEmailShare').executeTakeFirstOrThrow(), + ]); track(request, 'Page Viewed', { Page: 'Profile - Email Addresses', @@ -28,9 +54,57 @@ export async function loader({ request }: LoaderFunctionArgs) { return json({ emails, + student, }); } +const UpdateAllowEmailShare = z.object({ + allowEmailShare: z.preprocess((value) => value === '1', z.boolean()), +}); + +type UpdateAllowEmailShare = z.infer; + +export async function action({ request }: ActionFunctionArgs) { + const session = await ensureUserAuthenticated(request); + + const form = await request.formData(); + + const { data, errors } = validateForm( + UpdateAllowEmailShare, + Object.fromEntries(form) + ); + + if (!data) { + return json({ + error: '', + errors, + }); + } + + await db.transaction().execute(async (trx) => { + await updateAllowEmailShare(trx, user(session), data.allowEmailShare); + }); + + toast(session, { + message: 'Updated!', + type: 'success', + }); + + return json( + { + error: '', + errors, + }, + { + headers: { + 'Set-Cookie': await commitSession(session), + }, + } + ); +} + +const { allowEmailShare } = UpdateAllowEmailShare.keyof().enum; + export default function EmailsPage() { return ( <> @@ -41,7 +115,10 @@ export default function EmailsPage() { } function EmailAddressSection() { - const { emails } = useLoaderData(); + const { emails, student } = useLoaderData(); + const { errors } = getActionErrors(useActionData()); + + const submit = useSubmit(); const navigate = useNavigate(); @@ -78,7 +155,7 @@ function EmailAddressSection() { {email.email} {email.primary && ( - + Primary )} @@ -98,6 +175,26 @@ function EmailAddressSection() { )} + + submit(e.currentTarget)} + > + + + + ); } diff --git a/packages/core/src/infrastructure/database/migrations/2024-03-31T21:37:51Z-share-email.ts b/packages/core/src/infrastructure/database/migrations/2024-03-31T21:37:51Z-share-email.ts new file mode 100644 index 00000000..0f8b4ab2 --- /dev/null +++ b/packages/core/src/infrastructure/database/migrations/2024-03-31T21:37:51Z-share-email.ts @@ -0,0 +1,17 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely) { + await db.schema + .alterTable('students') + .addColumn('allow_email_share', 'boolean', (column) => { + return column.notNull().defaultTo(true); + }) + .execute(); +} + +export async function down(db: Kysely) { + await db.schema + .alterTable('students') + .dropColumn('allow_email_share') + .execute(); +} diff --git a/packages/core/src/member-profile.server.ts b/packages/core/src/member-profile.server.ts index b40f9c57..a67fb839 100644 --- a/packages/core/src/member-profile.server.ts +++ b/packages/core/src/member-profile.server.ts @@ -34,6 +34,7 @@ export { upsertIcebreakerResponses } from './modules/icebreaker/use-cases/upsert export { getAutocompletedCities } from './modules/location/queries/get-autocompleted-cities'; export { getCityDetails } from './modules/location/queries/get-city-details'; export { joinMemberDirectory } from './modules/member/use-cases/join-member-directory'; +export { updateAllowEmailShare } from './modules/member/use-cases/update-allow-email-share'; export { reportError } from './modules/sentry/use-cases/report-error'; export { countMessagesSent } from './modules/slack/queries/count-messages-sent'; export { claimSwagPack } from './modules/swag-pack/use-cases/claim-swag-pack'; diff --git a/packages/core/src/modules/member/use-cases/update-allow-email-share.ts b/packages/core/src/modules/member/use-cases/update-allow-email-share.ts new file mode 100644 index 00000000..6fb6e8ec --- /dev/null +++ b/packages/core/src/modules/member/use-cases/update-allow-email-share.ts @@ -0,0 +1,14 @@ +import { Transaction } from 'kysely'; +import { DB } from 'kysely-codegen/dist/db'; + +export async function updateAllowEmailShare( + trx: Transaction, + id: string, + allowEmailShareBool: boolean +) { + await trx + .updateTable('students') + .set({ allowEmailShare: allowEmailShareBool }) + .where('id', '=', id) + .execute(); +}