ColorStack Census '24
@@ -93,6 +93,7 @@ function CensusForm() {
const submit = useSubmit();
+ const [hasGraduated, setHasGraduated] = useState
(null);
const [hasInternship, setHasInternship] = useState(false);
return (
@@ -101,243 +102,322 @@ function CensusForm() {
method="post"
onBlur={(e) => submit(e.currentTarget)}
>
- {
- return (
+
+
- If your preferred email is not listed here, please add it{' '}
+ If you'd like to change your primary email, please add that email{' '}
here
{' '}
first.
- );
- })}
- error=""
- label="Email"
- labelFor="email"
- required
- >
-
-
+ }
+ error=""
+ label="Email"
+ labelFor="email"
+ required
+ >
+
+
+
-
-
-
+
+
+
+ setHasGraduated(e.currentTarget.value === '1')}
+ required
+ value="1"
+ />
+ setHasGraduated(e.currentTarget.value === '1')}
+ required
+ value="0"
+ />
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ {hasGraduated === false && (
+
+
-
-
-
-
-
- setHasInternship(e.currentTarget.value === '1')}
+ >
+
+
+ setHasInternship(e.currentTarget.value === '1')
+ }
+ required
+ value="1"
+ />
+
+ setHasInternship(e.currentTarget.value === '1')
+ }
+ required
+ value="0"
+ />
+
+
+
+ {hasInternship && (
+
+
+
+ )}
+
+ {hasInternship && (
+
+
+
+ )}
+
+
- setHasInternship(e.currentTarget.value === '1')}
+ >
+
+
+
+
-
-
+ >
+
+
+
+ )}
- {hasInternship && (
+
-
+ {iife(() => {
+ const resources = [
+ 'AlgoExpert',
+ 'Wiki',
+ 'Fam Fridays',
+ 'Slack',
+ 'Newsletter',
+ 'InterviewPen',
+ 'CompSciLib',
+ ];
+
+ return (
+
+ {resources.map((resource) => {
+ return (
+
+ );
+ })}
+
+ );
+ })}
- )}
- {hasInternship && (
-
+
- )}
-
-
-
-
-
- {iife(() => {
- const resources = [
- 'AlgoExpert',
- 'Wiki',
- 'Fam Fridays',
- 'Slack',
- 'Newsletter',
- 'InterviewPen',
- 'CompSciLib',
- ];
-
- return (
-
- {resources.map((resource) => {
- return (
-
- );
- })}
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {iife(() => {
- const categories = [
- 'Career development (interview prep, resume review, etc.)',
- 'Access to opportunities',
- 'Academic help',
- 'Fellowship + networking',
- ];
-
- return (
-
- {categories.map((category) => {
- return (
-
- );
- })}
-
- );
- })}
-
+ >
+ {iife(() => {
+ const categories = [
+ 'Career development (interview prep, resume review, etc.)',
+ 'Access to opportunities',
+ 'Academic help',
+ 'Fellowship + networking',
+ ];
+
+ return (
+
+ {categories.map((category) => {
+ return (
+
+ );
+ })}
+
+ );
+ })}
+
+
);
}
+function CensusSection({
+ children,
+ last = false,
+ title,
+}: PropsWithChildren<{
+ last?: boolean;
+ title: string;
+}>) {
+ return (
+
+
+ {title}
+
+
+ {children}
+
+ {!last && }
+
+ );
+}
+
function AgreeRating({ name }: Pick, 'name'>) {
const ratings = [
'Strongly agree',
diff --git a/packages/core/src/modules/authentication/use-cases/login-with-oauth.ts b/packages/core/src/modules/authentication/use-cases/login-with-oauth.ts
index e5d16e90..9463afde 100644
--- a/packages/core/src/modules/authentication/use-cases/login-with-oauth.ts
+++ b/packages/core/src/modules/authentication/use-cases/login-with-oauth.ts
@@ -1,7 +1,7 @@
import { match } from 'ts-pattern';
import { db } from '@/infrastructure/database';
-import { findMemberByEmail } from '@/modules/member/queries/find-member-by-email';
+import { getMemberByEmail } from '@/modules/member/queries/get-member-by-email';
import { signToken } from '@/shared/utils/auth.utils';
import { type OAuthCodeState } from '../authentication.types';
import { type OAuthServiceType } from '../oauth.service';
@@ -43,7 +43,7 @@ export async function loginWithOAuth(input: OAuthLoginInput) {
.where('deletedAt', 'is', null)
.executeTakeFirst();
} else {
- entity = await findMemberByEmail(email);
+ entity = await getMemberByEmail(email);
}
if (!entity) {
diff --git a/packages/core/src/modules/event/use-cases/sync-airmeet-event.ts b/packages/core/src/modules/event/use-cases/sync-airmeet-event.ts
index 99d19db6..ab1afa71 100644
--- a/packages/core/src/modules/event/use-cases/sync-airmeet-event.ts
+++ b/packages/core/src/modules/event/use-cases/sync-airmeet-event.ts
@@ -1,7 +1,7 @@
import { type GetBullJobData } from '@/infrastructure/bull/bull.types';
import { job } from '@/infrastructure/bull/use-cases/job';
import { db } from '@/infrastructure/database';
-import { findMemberByEmail } from '@/modules/member/queries/find-member-by-email';
+import { getMemberByEmail } from '@/modules/member/queries/get-member-by-email';
import { NotFoundError } from '@/shared/errors';
import {
getAirmeetEvent,
@@ -43,7 +43,7 @@ export async function syncAirmeetEvent({
await Promise.all(
attendees.map(async (attendee) => {
- const student = await findMemberByEmail(attendee.email);
+ const student = await getMemberByEmail(attendee.email);
await trx
.insertInto('eventAttendees')
diff --git a/packages/core/src/modules/location/location.shared.ts b/packages/core/src/modules/location/location.shared.ts
index 2d7de6f0..46666882 100644
--- a/packages/core/src/modules/location/location.shared.ts
+++ b/packages/core/src/modules/location/location.shared.ts
@@ -1,5 +1,3 @@
-import { z } from 'zod';
-
import { ErrorWithContext } from '@/shared/errors';
// Constants
@@ -11,14 +9,25 @@ export const GOOGLE_PLACES_API_URL =
export class GooglePlacesError extends ErrorWithContext {}
+const GOOGLE_MAPS_API_KEY_MISSING_MESSAGE =
+ '"GOOGLE_MAPS_API_KEY" is not set, so Google Maps API is disabled.';
+
+export class GoogleKeyMissingError extends ErrorWithContext {
+ constructor() {
+ super(GOOGLE_MAPS_API_KEY_MISSING_MESSAGE);
+ }
+}
+
// Helpers
export function getGoogleMapsKey() {
- const result = z.string().min(1).safeParse(process.env.GOOGLE_MAPS_API_KEY);
+ const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;
+
+ if (!GOOGLE_MAPS_API_KEY) {
+ console.warn(GOOGLE_MAPS_API_KEY_MISSING_MESSAGE);
- if (!result.success) {
- throw new Error('Please provide a valid Google Maps API key.');
+ return null;
}
- return result.data;
+ return GOOGLE_MAPS_API_KEY;
}
diff --git a/packages/core/src/modules/location/queries/get-autocompleted-cities.ts b/packages/core/src/modules/location/queries/get-autocompleted-cities.ts
index 297297a1..5a8d0fa6 100644
--- a/packages/core/src/modules/location/queries/get-autocompleted-cities.ts
+++ b/packages/core/src/modules/location/queries/get-autocompleted-cities.ts
@@ -28,7 +28,13 @@ const GoogleAutocompleteData = z.object({
export async function getAutocompletedCities(search: string) {
const url = new URL(GOOGLE_PLACES_API_URL + '/autocomplete/json');
- url.searchParams.set('key', getGoogleMapsKey());
+ const key = getGoogleMapsKey();
+
+ if (!key) {
+ return [];
+ }
+
+ url.searchParams.set('key', key);
url.searchParams.set('input', search);
url.searchParams.set(
'types',
diff --git a/packages/core/src/modules/location/queries/get-city-details.ts b/packages/core/src/modules/location/queries/get-city-details.ts
index 4848781a..9da2498e 100644
--- a/packages/core/src/modules/location/queries/get-city-details.ts
+++ b/packages/core/src/modules/location/queries/get-city-details.ts
@@ -3,6 +3,7 @@ import { z } from 'zod';
import {
getGoogleMapsKey,
GOOGLE_PLACES_API_URL,
+ GoogleKeyMissingError,
GooglePlacesError,
} from '../location.shared';
@@ -31,7 +32,13 @@ const GooglePlaceDetailsResponse = z.object({
export async function getCityDetails(id: string) {
const url = new URL(GOOGLE_PLACES_API_URL + '/details/json');
- url.searchParams.set('key', getGoogleMapsKey());
+ const key = getGoogleMapsKey();
+
+ if (!key) {
+ throw new GoogleKeyMissingError();
+ }
+
+ url.searchParams.set('key', key);
url.searchParams.set('fields', 'geometry,name');
url.searchParams.set('place_id', id);
diff --git a/packages/core/src/modules/mailchimp/use-cases/sync-mailchimp-campaign.ts b/packages/core/src/modules/mailchimp/use-cases/sync-mailchimp-campaign.ts
index dd21d8ef..c4c722e1 100644
--- a/packages/core/src/modules/mailchimp/use-cases/sync-mailchimp-campaign.ts
+++ b/packages/core/src/modules/mailchimp/use-cases/sync-mailchimp-campaign.ts
@@ -11,7 +11,7 @@ import { id } from '@oyster/utils';
import { type GetBullJobData } from '@/infrastructure/bull/bull.types';
import { job } from '@/infrastructure/bull/use-cases/job';
import { db } from '@/infrastructure/database';
-import { findMemberByEmail } from '@/modules/member/queries/find-member-by-email';
+import { getMemberByEmail } from '@/modules/member/queries/get-member-by-email';
import { NotFoundError } from '@/shared/errors';
export async function syncMailchimpCampaign({
@@ -152,7 +152,7 @@ async function getActivityData(campaignId: string) {
// we can find them.
for (const click of result.clicks) {
- const student = await findMemberByEmail(click.email);
+ const student = await getMemberByEmail(click.email);
if (student) {
click.studentId = student.id;
@@ -160,7 +160,7 @@ async function getActivityData(campaignId: string) {
}
for (const open of result.opens) {
- const student = await findMemberByEmail(open.email);
+ const student = await getMemberByEmail(open.email);
if (student) {
open.studentId = student.id;
diff --git a/packages/core/src/modules/member/queries/find-member-by-email.ts b/packages/core/src/modules/member/queries/get-member-by-email.ts
similarity index 84%
rename from packages/core/src/modules/member/queries/find-member-by-email.ts
rename to packages/core/src/modules/member/queries/get-member-by-email.ts
index 86b7b72f..d262d878 100644
--- a/packages/core/src/modules/member/queries/find-member-by-email.ts
+++ b/packages/core/src/modules/member/queries/get-member-by-email.ts
@@ -1,6 +1,6 @@
import { db } from '@/infrastructure/database';
-export function findMemberByEmail(email: string) {
+export function getMemberByEmail(email: string) {
return db
.selectFrom('students')
.leftJoin('studentEmails', 'studentEmails.studentId', 'students.id')
diff --git a/packages/core/src/modules/member/queries/list-emails.ts b/packages/core/src/modules/member/queries/list-emails.ts
index 6b688ca2..471a80c9 100644
--- a/packages/core/src/modules/member/queries/list-emails.ts
+++ b/packages/core/src/modules/member/queries/list-emails.ts
@@ -1,6 +1,13 @@
import { sql } from 'kysely';
+import { z } from 'zod';
import { db } from '@oyster/db';
+import { Email } from '@oyster/types';
+
+const EmailResult = z.object({
+ email: Email,
+ primary: z.boolean(),
+});
export async function listEmails(memberId: string) {
const rows = await db
@@ -13,5 +20,7 @@ export async function listEmails(memberId: string) {
.where('students.id', '=', memberId)
.execute();
- return rows;
+ const emails = EmailResult.array().parse(rows);
+
+ return emails;
}
diff --git a/packages/core/src/modules/slack/events/slack-workspace-joined.ts b/packages/core/src/modules/slack/events/slack-workspace-joined.ts
index 5b54736e..d2c08aca 100644
--- a/packages/core/src/modules/slack/events/slack-workspace-joined.ts
+++ b/packages/core/src/modules/slack/events/slack-workspace-joined.ts
@@ -1,13 +1,13 @@
import { type GetBullJobData } from '@/infrastructure/bull/bull.types';
import { db } from '@/infrastructure/database';
-import { findMemberByEmail } from '@/modules/member/queries/find-member-by-email';
+import { getMemberByEmail } from '@/modules/member/queries/get-member-by-email';
import { NotFoundError } from '@/shared/errors';
export async function onSlackWorkspaceJoined({
email,
slackId,
}: GetBullJobData<'slack.joined'>) {
- const member = await findMemberByEmail(email);
+ const member = await getMemberByEmail(email);
if (!member) {
throw new NotFoundError(
diff --git a/packages/core/src/modules/survey/use-cases/import-survey-responses.ts b/packages/core/src/modules/survey/use-cases/import-survey-responses.ts
index 03acf653..80ba991a 100644
--- a/packages/core/src/modules/survey/use-cases/import-survey-responses.ts
+++ b/packages/core/src/modules/survey/use-cases/import-survey-responses.ts
@@ -5,7 +5,7 @@ import { id } from '@oyster/utils';
import { job } from '@/infrastructure/bull/use-cases/job';
import { db } from '@/infrastructure/database';
-import { findMemberByEmail } from '@/modules/member/queries/find-member-by-email';
+import { getMemberByEmail } from '@/modules/member/queries/get-member-by-email';
import { parseCsv } from '@/shared/utils/csv.utils';
import { AddSurveyResponseInput, SurveyResponse } from '../survey.types';
@@ -39,7 +39,7 @@ export async function importSurveyResponses(
'Responded On': respondedOn,
} = result.data;
- const student = await findMemberByEmail(email);
+ const student = await getMemberByEmail(email);
return AddSurveyResponseInput.parse({
email,
diff --git a/packages/ui/src/components/radio.tsx b/packages/ui/src/components/radio.tsx
index 33538b93..479c77f4 100644
--- a/packages/ui/src/components/radio.tsx
+++ b/packages/ui/src/components/radio.tsx
@@ -76,10 +76,11 @@ export const Radio = ({
};
Radio.Group = function RadioGroup({ children }: PropsWithChildren) {
- const childrenWithProps = React.Children.map(children, (child, i: number) => {
+ const childrenWithProps = React.Children.map(children, (child, i) => {
if (React.isValidElement(child)) {
const props: Partial = {
- color: ACCENT_COLORS[i % ACCENT_COLORS.length],
+ ...child.props,
+ color: child.props.color || ACCENT_COLORS[i % ACCENT_COLORS.length],
};
return React.cloneElement(child, props);
diff --git a/packages/ui/src/hooks/use-revalidate-on-focus.ts b/packages/ui/src/hooks/use-revalidate-on-focus.ts
new file mode 100644
index 00000000..b8f61719
--- /dev/null
+++ b/packages/ui/src/hooks/use-revalidate-on-focus.ts
@@ -0,0 +1,25 @@
+import { useRevalidator } from '@remix-run/react';
+import { useEffect } from 'react';
+
+/**
+ * Revalidates the current Remix route when the window is focused or visibility
+ * changes. This is useful for revalidating data when the user returns to the
+ * tab, acting similar to the SWR `revalidateOnFocus` option.
+ */
+export function useRevalidateOnFocus() {
+ const revalidator = useRevalidator();
+
+ useEffect(() => {
+ function onFocus() {
+ revalidator.revalidate();
+ }
+
+ window.addEventListener('focus', onFocus);
+ window.addEventListener('visibilitychange', onFocus);
+
+ return () => {
+ window.removeEventListener('focus', onFocus);
+ window.removeEventListener('visibilitychange', onFocus);
+ };
+ }, [revalidator]);
+}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 49a88b91..8d530ff6 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -48,6 +48,7 @@ export type { ToastProps } from './components/toast';
export { useDelayedValue } from './hooks/use-delayed-value';
export { useHydrated } from './hooks/use-hydrated';
export { useOnClickOutside } from './hooks/use-on-click-outside';
+export { useRevalidateOnFocus } from './hooks/use-revalidate-on-focus';
export { useSearchParams } from './hooks/use-search-params';
export { ACCENT_COLORS } from './utils/constants';
export type { AccentColor } from './utils/constants';
From 6b8996b0c882dbb4f034a251ac8b90beda29ed98 Mon Sep 17 00:00:00 2001
From: Rami Abdou <38056800+ramiAbdou@users.noreply.github.com>
Date: Thu, 11 Apr 2024 15:45:03 -0700
Subject: [PATCH 003/178] =?UTF-8?q?feat:=20add=20alumni=20fields=20to=20ce?=
=?UTF-8?q?nsus=20form=20(3/x)=20=F0=9F=93=9D=20=20(#132)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/routes/_profile.census.tsx | 631 +++++++++++-------
1 file changed, 402 insertions(+), 229 deletions(-)
diff --git a/apps/member-profile/app/routes/_profile.census.tsx b/apps/member-profile/app/routes/_profile.census.tsx
index 574f6171..daa38794 100644
--- a/apps/member-profile/app/routes/_profile.census.tsx
+++ b/apps/member-profile/app/routes/_profile.census.tsx
@@ -9,7 +9,7 @@ import {
useLoaderData,
useSubmit,
} from '@remix-run/react';
-import { type PropsWithChildren, useState } from 'react';
+import React, { type PropsWithChildren, useContext, useState } from 'react';
import { z } from 'zod';
import { db } from '@oyster/db';
@@ -88,9 +88,21 @@ export default function CensusPage() {
);
}
-function CensusForm() {
- const { emails, primaryEmail } = useLoaderData();
+type CensusContext = {
+ hasGraduated: boolean | null;
+ hasInternship: boolean | null;
+ setHasGraduated(value: boolean): void;
+ setHasInternship(value: boolean): void;
+};
+
+const CensusContext = React.createContext({
+ hasGraduated: null,
+ hasInternship: null,
+ setHasGraduated: (_: boolean) => {},
+ setHasInternship: (_: boolean) => {},
+});
+function CensusForm() {
const submit = useSubmit();
const [hasGraduated, setHasGraduated] = useState(null);
@@ -102,309 +114,470 @@ function CensusForm() {
method="post"
onBlur={(e) => submit(e.currentTarget)}
>
-
-
- If you'd like to change your primary email, please add that email{' '}
-
- here
- {' '}
- first.
-
- }
- error=""
- label="Email"
- labelFor="email"
+
+
+
+
+
+
+
+ );
+}
+
+function BasicSection() {
+ const { emails, primaryEmail } = useLoaderData();
+
+ return (
+
+
+ If you'd like to change your primary email, please add that email{' '}
+
+ here
+ {' '}
+ first.
+
+ }
+ error=""
+ label="Email"
+ labelFor="email"
+ required
+ >
+
+
+
+
+
-
-
-
+ />
+
+
+ );
+}
+
+function EducationSection() {
+ const { hasGraduated, setHasGraduated } = useContext(CensusContext);
+
+ return (
+
+
+
+ setHasGraduated(e.currentTarget.value === '1')}
+ required
+ value="1"
+ />
+ setHasGraduated(e.currentTarget.value === '1')}
+ required
+ value="0"
+ />
+
+
-
+ {hasGraduated === true && (
setHasGraduated(e.currentTarget.value === '1')}
+ name="hasTechnicalDegree"
required
value="1"
/>
setHasGraduated(e.currentTarget.value === '1')}
+ name="hasTechnicalDegree"
required
value="0"
/>
-
- {hasGraduated === false && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
+ )}
{hasGraduated === false && (
-
+ <>
+
+
+
+
- setHasInternship(e.currentTarget.value === '1')
- }
+ name="isInternationalStudent"
required
value="1"
/>
- setHasInternship(e.currentTarget.value === '1')
- }
+ name="isInternationalStudent"
required
value="0"
/>
- {hasInternship && (
-
-
-
- )}
-
- {hasInternship && (
-
-
-
- )}
+
+
+
-
+
-
+
-
+ >
)}
+
+ );
+}
-
-
- {iife(() => {
- const resources = [
- 'AlgoExpert',
- 'Wiki',
- 'Fam Fridays',
- 'Slack',
- 'Newsletter',
- 'InterviewPen',
- 'CompSciLib',
- ];
+function WorkSection() {
+ const { hasGraduated, hasInternship, setHasInternship } =
+ useContext(CensusContext);
- return (
-
- {resources.map((resource) => {
- return (
-
- );
- })}
-
- );
- })}
-
+ if (hasGraduated === null) {
+ return null;
+ }
+
+ return hasGraduated ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ setHasInternship(e.currentTarget.value === '1')}
+ required
+ value="1"
+ />
+ setHasInternship(e.currentTarget.value === '1')}
+ required
+ value="0"
+ />
+
+
+ {hasInternship && (
-
+
+ )}
+ {hasInternship && (
- {iife(() => {
- const categories = [
- 'Career development (interview prep, resume review, etc.)',
- 'Access to opportunities',
- 'Academic help',
- 'Fellowship + networking',
- ];
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+function ColorStackFeedbackSection() {
+ const { hasGraduated } = useContext(CensusContext);
+ return (
+
+
+
+ {[
+ 'AlgoExpert',
+ 'CompSciLib',
+ 'Fam Fridays',
+ 'InterviewPen',
+ 'Newsletter',
+ 'Slack',
+ 'Wiki',
+ ].map((resource) => {
return (
-
- {categories.map((category) => {
- return (
-
- );
- })}
-
+
);
})}
-
-
-
+
+
+
+ {hasGraduated && (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {hasGraduated === false && (
+ <>
+
+
+
+
+
+ {iife(() => {
+ return (
+
+ {[
+ 'Career development (interview prep, resume review, etc.)',
+ 'Access to opportunities',
+ 'Academic help',
+ 'Fellowship + networking',
+ ].map((category) => {
+ return (
+
+ );
+ })}
+
+ );
+ })}
+
+ >
+ )}
+
);
}
-function CensusSection({
- children,
- last = false,
- title,
-}: PropsWithChildren<{
+type CensusSectionProps = PropsWithChildren<{
last?: boolean;
title: string;
-}>) {
+}>;
+
+function CensusSection({ children, last = false, title }: CensusSectionProps) {
return (
From 979634eafdb2462be3d2000a48119bd385471363 Mon Sep 17 00:00:00 2001
From: Rami Abdou <38056800+ramiAbdou@users.noreply.github.com>
Date: Fri, 12 Apr 2024 04:24:27 -0700
Subject: [PATCH 004/178] =?UTF-8?q?feat:=20add=20cookie=20storage=20to=20c?=
=?UTF-8?q?ensus=20form=20=F0=9F=8D=AA=20(#133)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/routes/_profile.census.tsx | 461 +++++++++++++-----
apps/member-profile/app/shared/constants.ts | 1 +
packages/core/src/member-profile.server.ts | 1 +
packages/core/src/member-profile.ui.ts | 1 +
.../core/src/modules/census/census.types.ts | 37 ++
.../use-cases/submit-census-response.ts | 8 +
packages/types/src/index.ts | 2 +
packages/types/src/shared/zod.ts | 21 +
8 files changed, 397 insertions(+), 135 deletions(-)
create mode 100644 packages/core/src/modules/census/census.types.ts
create mode 100644 packages/core/src/modules/census/use-cases/submit-census-response.ts
diff --git a/apps/member-profile/app/routes/_profile.census.tsx b/apps/member-profile/app/routes/_profile.census.tsx
index daa38794..459a023b 100644
--- a/apps/member-profile/app/routes/_profile.census.tsx
+++ b/apps/member-profile/app/routes/_profile.census.tsx
@@ -2,22 +2,29 @@ import {
type ActionFunctionArgs,
json,
type LoaderFunctionArgs,
+ redirect,
} from '@remix-run/node';
+import { createCookie } from '@remix-run/node';
import {
Link,
Form as RemixForm,
+ useActionData,
useLoaderData,
+ useNavigation,
useSubmit,
} from '@remix-run/react';
import React, { type PropsWithChildren, useContext, useState } from 'react';
-import { z } from 'zod';
+import { match } from 'ts-pattern';
+import { type z } from 'zod';
import { db } from '@oyster/db';
import {
+ Button,
Checkbox,
Divider,
type FieldProps,
Form,
+ getActionErrors,
Input,
Radio,
Select,
@@ -26,13 +33,29 @@ import {
useRevalidateOnFocus,
validateForm,
} from '@oyster/ui';
-import { iife } from '@oyster/utils';
import { CityCombobox } from '../shared/components/city-combobox';
import { Route } from '../shared/constants';
import { listEmails } from '../shared/core.server';
+import { SchoolCombobox, SubmitCensusResponseInput } from '../shared/core.ui';
+import { getMember } from '../shared/queries';
import { ensureUserAuthenticated, user } from '../shared/session.server';
+const censusCookie = createCookie('census', {
+ maxAge: 60 * 60 * 24 * 30,
+ secure: true,
+});
+
+const CensusCookieObject = SubmitCensusResponseInput.partial();
+
+async function getCensusCookie(request: Request) {
+ const cookieHeader = request.headers.get('Cookie');
+ const parsedCookie = await censusCookie.parse(cookieHeader);
+ const cookie = CensusCookieObject.parse(parsedCookie);
+
+ return cookie;
+}
+
export async function loader({ request }: LoaderFunctionArgs) {
const session = await ensureUserAuthenticated(request);
@@ -40,14 +63,39 @@ export async function loader({ request }: LoaderFunctionArgs) {
const [emails] = await Promise.all([listEmails(memberId)]);
- const primaryEmail = emails.find((email) => {
- return !!email.primary;
- })!;
+ let cookie: z.infer;
+
+ try {
+ cookie = await getCensusCookie(request);
+ } catch (e) {
+ const { email } = emails.find((email) => {
+ return !!email.primary;
+ })!;
+
+ const { schoolId, school: schoolName } = await getMember(memberId, {
+ school: true,
+ })
+ .select(['students.schoolId'])
+ .executeTakeFirstOrThrow();
+
+ cookie = {
+ email,
+ schoolId: schoolId || undefined,
+ schoolName: schoolName || undefined,
+ };
+ }
- return json({
- emails,
- primaryEmail: primaryEmail.email,
- });
+ return json(
+ {
+ emails,
+ progress: cookie,
+ },
+ {
+ headers: {
+ 'Set-Cookie': await censusCookie.serialize(cookie),
+ },
+ }
+ );
}
export async function action({ request }: ActionFunctionArgs) {
@@ -55,7 +103,18 @@ export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
- const { data, errors } = validateForm(z.object({}), Object.fromEntries(form));
+ const values = {
+ ...Object.fromEntries(form),
+ ...(!!form.get('currentResources') && {
+ currentResources: form.getAll('currentResources'),
+ }),
+ };
+
+ const isSave = form.get('intent') === 'save';
+
+ const { data, errors } = isSave
+ ? validateForm(CensusCookieObject, values)
+ : validateForm(SubmitCensusResponseInput, values);
if (!data) {
return json({
@@ -64,11 +123,35 @@ export async function action({ request }: ActionFunctionArgs) {
});
}
+ if (isSave) {
+ const existingCookie = await getCensusCookie(request);
+
+ const cookie = {
+ ...existingCookie,
+ ...data,
+ };
+
+ return json(
+ {
+ error: '',
+ errors,
+ },
+ {
+ headers: {
+ 'Set-Cookie': await censusCookie.serialize(cookie),
+ },
+ }
+ );
+ }
+
+ // Need to handle the actual submit as well...
+
await db.transaction().execute(async (_) => {});
- return json({
- error: '',
- errors,
+ return redirect(Route['/census/confirmation'], {
+ headers: {
+ 'Set-Cookie': await censusCookie.serialize('', { maxAge: 1 }),
+ },
});
}
@@ -91,22 +174,38 @@ export default function CensusPage() {
type CensusContext = {
hasGraduated: boolean | null;
hasInternship: boolean | null;
+ isOtherSchool: boolean;
setHasGraduated(value: boolean): void;
setHasInternship(value: boolean): void;
+ setIsOtherSchool(value: boolean): void;
};
const CensusContext = React.createContext({
hasGraduated: null,
hasInternship: null,
+ isOtherSchool: false,
setHasGraduated: (_: boolean) => {},
setHasInternship: (_: boolean) => {},
+ setIsOtherSchool: (_: boolean) => {},
});
+const keys = SubmitCensusResponseInput.keyof().enum;
+
function CensusForm() {
+ const { progress } = useLoaderData();
+ const { error } = getActionErrors(useActionData());
const submit = useSubmit();
+ const submitting = useNavigation().state === 'submitting';
- const [hasGraduated, setHasGraduated] = useState(null);
- const [hasInternship, setHasInternship] = useState(false);
+ const [hasGraduated, setHasGraduated] = useState(
+ progress.hasGraduated ?? null
+ );
+
+ const [hasInternship, setHasInternship] = useState(
+ progress.hasInternship ?? false
+ );
+
+ const [isOtherSchool, setIsOtherSchool] = useState(false);
return (
+
+ {error}
+
+
+
+
);
}
function BasicSection() {
- const { emails, primaryEmail } = useLoaderData();
+ const { emails, progress } = useLoaderData();
+ const { errors } = getActionErrors(useActionData());
return (
- If you'd like to change your primary email, please add that email{' '}
+ If you'd like to change your primary email but it's not listed
+ below, please add it{' '}
here
{' '}
first.
}
- error=""
+ error={errors.email}
label="Email"
- labelFor="email"
+ labelFor={keys.email}
required
>
-
);
}
diff --git a/packages/core/src/member-profile.server.ts b/packages/core/src/member-profile.server.ts
index 763b68a8..fe27d56f 100644
--- a/packages/core/src/member-profile.server.ts
+++ b/packages/core/src/member-profile.server.ts
@@ -9,6 +9,7 @@ export {
} from './modules/authentication/shared/oauth.utils';
export { sendOneTimeCode } from './modules/authentication/use-cases/send-one-time-code';
export { verifyOneTimeCode } from './modules/authentication/use-cases/verify-one-time-code';
+export { getCensusResponse } from './modules/census/queries/get-census-response';
export { submitCensusResponse } from './modules/census/use-cases/submit-census-response';
export { listSchools } from './modules/education/queries/list-schools';
export { addEducation } from './modules/education/use-cases/add-education';
diff --git a/packages/core/src/modules/census/queries/get-census-response.ts b/packages/core/src/modules/census/queries/get-census-response.ts
new file mode 100644
index 00000000..30b16023
--- /dev/null
+++ b/packages/core/src/modules/census/queries/get-census-response.ts
@@ -0,0 +1,25 @@
+import { type SelectExpression } from 'kysely';
+
+import { type DB } from '@oyster/db';
+
+import { db } from '@/infrastructure/database';
+
+type GetCensusResponseOptions