From 8830c235797a8fe61ab4ea8b21d3747e1d504b8d Mon Sep 17 00:00:00 2001 From: Arshadul Monir Date: Mon, 10 Aug 2026 14:29:16 -0400 Subject: [PATCH 1/2] Added members list view for admin users --- js/src/app/router/router.tsx | 2 + js/src/features/members/Members.page.test.tsx | 32 +++++ js/src/features/members/Members.page.tsx | 114 ++++++++++++++++++ js/src/features/members/api/members.mock.ts | 46 +++++++ js/src/features/members/api/useMembers.ts | 39 ++++++ js/src/lib/api/useSession.ts | 15 ++- js/src/lib/test/server.ts | 3 +- .../api/auth/security/SecurityConfig.java | 4 +- .../patchats/api/member/MemberController.java | 10 ++ .../api/member/MemberControllerTest.java | 71 ++++++++--- .../api/member/MemberServiceTest.java | 36 +++++- 11 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 js/src/features/members/Members.page.test.tsx create mode 100644 js/src/features/members/Members.page.tsx create mode 100644 js/src/features/members/api/members.mock.ts create mode 100644 js/src/features/members/api/useMembers.ts diff --git a/js/src/app/router/router.tsx b/js/src/app/router/router.tsx index bb70f14..e9b77f3 100644 --- a/js/src/app/router/router.tsx +++ b/js/src/app/router/router.tsx @@ -7,6 +7,7 @@ import AdminPage from "@/features/admin/Admin.page"; import AdminLoginPage from "@/features/admin/AdminLogin.page"; import EmailAdminPage from "@/features/emails/EmailAdminPage"; import HomePage from "@/features/home/Home.page"; +import MembersPage from "@/features/members/Members.page"; import SamplePage from "@/features/sample/Sample.page"; import SampleAdminPage from "@/features/sample/SampleAdmin.page"; import { SignUpPage } from "@/features/sign-up/SignUp.page"; @@ -41,6 +42,7 @@ export const router = createBrowserRouter([ element: , children: [ { path: "admin", element: }, + { path: "admin/members", element: }, { path: "admin/email", element: }, { path: "sample/admin", element: }, ], diff --git a/js/src/features/members/Members.page.test.tsx b/js/src/features/members/Members.page.test.tsx new file mode 100644 index 0000000..a877713 --- /dev/null +++ b/js/src/features/members/Members.page.test.tsx @@ -0,0 +1,32 @@ +import MembersPage from "@/features/members/Members.page"; +import { renderWithProviders, screen } from "@/lib/test/render"; +import { server } from "@/lib/test/server"; +import { http, HttpResponse } from "msw"; +import { expect, test } from "vitest"; + +test("renders every member returned by the API", async () => { + renderWithProviders(); + + expect(await screen.findByText("Alex Morgan")).toBeInTheDocument(); + expect(screen.getByText("Jordan Lee")).toBeInTheDocument(); + expect(screen.getByText("2 members")).toBeInTheDocument(); + expect(screen.getByText("Active")).toBeInTheDocument(); + expect(screen.getByText("Inactive")).toBeInTheDocument(); +}); + +test("renders an empty state when there are no members", async () => { + server.use( + http.get("/api/members", () => + HttpResponse.json({ + message: "Members retrieved successfully", + payload: [], + success: true, + }), + ), + ); + + renderWithProviders(); + + expect(await screen.findByText("No members found.")).toBeInTheDocument(); + expect(screen.getByText("0 members")).toBeInTheDocument(); +}); diff --git a/js/src/features/members/Members.page.tsx b/js/src/features/members/Members.page.tsx new file mode 100644 index 0000000..616e168 --- /dev/null +++ b/js/src/features/members/Members.page.tsx @@ -0,0 +1,114 @@ +import { useMembers } from "@/features/members/api/useMembers"; +import { + Alert, + Anchor, + Badge, + Center, + Group, + Loader, + Paper, + Stack, + Table, + Text, + Title, +} from "@mantine/core"; + +const dateFormatter = new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", +}); + +export default function MembersPage() { + const { data: members, isError, isPending } = useMembers(); + + if (isPending) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + Try refreshing the page. If the problem continues, check that the + backend is running. + + ); + } + + return ( + + +
+ Members + Everyone registered for PatChats. +
+ + {members.length} {members.length === 1 ? "member" : "members"} + +
+ {members.length === 0 ? + + + No members found. + + + : + + + + Name + Email + LinkedIn + Status + Match preference + Industry + Role + Topics + Joined + + + + {members.map((member) => ( + + + {member.firstName} {member.lastName} + + + + {member.email} + + + + {member.linkedInUrl ? + + Profile + + : "—"} + + + + {member.active ? "Active" : "Inactive"} + + + {member.matchPref ?? "—"} + {member.industryPref ?? "—"} + {member.rolePref ?? "—"} + {member.topics ?? "—"} + + {dateFormatter.format(new Date(member.createdAt))} + + + ))} + +
+
+ } +
+ ); +} diff --git a/js/src/features/members/api/members.mock.ts b/js/src/features/members/api/members.mock.ts new file mode 100644 index 0000000..3ade4da --- /dev/null +++ b/js/src/features/members/api/members.mock.ts @@ -0,0 +1,46 @@ +import { http, HttpResponse } from "msw"; + +export const membersHandlers = [ + http.get("/api/members", () => + HttpResponse.json({ + message: "Members retrieved successfully", + payload: [ + { + active: true, + createdAt: "2026-01-15T14:30:00Z", + email: "alex@example.com", + extraNotes: null, + firstName: "Alex", + id: "50ecf8a0-6345-40f8-b59f-438c3f338b82", + industryPref: "Technology", + introduction: "I build community-focused software.", + lastName: "Morgan", + linkedInUrl: "https://www.linkedin.com/in/alex-morgan", + matchPref: "Peer", + referralSource: "Patina Network", + rolePref: "Engineering", + topics: "Community, Technology", + updatedAt: "2026-01-15T14:30:00Z", + }, + { + active: false, + createdAt: "2025-12-10T09:00:00Z", + email: "jordan@example.com", + extraNotes: "Prefers virtual chats.", + firstName: "Jordan", + id: "db827ce4-5ed1-4649-98ca-3e5fb538d22e", + industryPref: "Design", + introduction: "I am a product designer.", + lastName: "Lee", + linkedInUrl: null, + matchPref: "Mentor", + referralSource: null, + rolePref: "Product design", + topics: "Design systems", + updatedAt: "2026-02-01T11:00:00Z", + }, + ], + success: true, + }), + ), +]; diff --git a/js/src/features/members/api/useMembers.ts b/js/src/features/members/api/useMembers.ts new file mode 100644 index 0000000..b9fe7d4 --- /dev/null +++ b/js/src/features/members/api/useMembers.ts @@ -0,0 +1,39 @@ +import { apiFetch } from "@/lib/api/client"; +import { useQuery } from "@tanstack/react-query"; + +export interface Member { + active: boolean; + createdAt: string; + email: string; + extraNotes: string | null; + firstName: string; + id: string; + industryPref: string | null; + introduction: string; + lastName: string; + linkedInUrl: string | null; + matchPref: string | null; + referralSource: string | null; + rolePref: string | null; + topics: string | null; + updatedAt: string; +} + +interface MembersResponse { + message: string; + payload: Member[]; + success: boolean; +} + +export const membersQueryKey = ["members"] as const; + +/** Fetches every member for the admin members page. */ +export function useMembers() { + return useQuery({ + queryKey: membersQueryKey, + queryFn: async () => { + const response = await apiFetch("/members"); + return response.payload; + }, + }); +} diff --git a/js/src/lib/api/useSession.ts b/js/src/lib/api/useSession.ts index f48ec69..bcbcfc6 100644 --- a/js/src/lib/api/useSession.ts +++ b/js/src/lib/api/useSession.ts @@ -19,7 +19,20 @@ export const sessionQueryKey = ["session"] as const; export function useSession() { return useQuery({ queryKey: sessionQueryKey, - queryFn: () => apiFetch("/session"), + queryFn: () => { + // Authentication is not wired yet. Local development uses an admin + // identity so guarded pages can be exercised; production still fails + // closed when the session endpoint is unavailable. + if (import.meta.env.DEV) { + return Promise.resolve({ + id: "local-dev-admin", + isAdmin: true, + name: "Local admin", + }); + } + + return apiFetch("/session"); + }, retry: false, staleTime: Infinity, }); diff --git a/js/src/lib/test/server.ts b/js/src/lib/test/server.ts index 3c2f213..f51ed1c 100644 --- a/js/src/lib/test/server.ts +++ b/js/src/lib/test/server.ts @@ -1,3 +1,4 @@ +import { membersHandlers } from "@/features/members/api/members.mock"; import { sampleHandlers } from "@/features/sample/api/sample.mock"; import { setupServer } from "msw/node"; @@ -5,4 +6,4 @@ import { setupServer } from "msw/node"; * MSW server for tests. Each domain owns its request handlers in * `features//api/.mock.ts`; compose them here. */ -export const server = setupServer(...sampleHandlers); +export const server = setupServer(...membersHandlers, ...sampleHandlers); diff --git a/src/main/java/org/patinanetwork/patchats/api/auth/security/SecurityConfig.java b/src/main/java/org/patinanetwork/patchats/api/auth/security/SecurityConfig.java index 3b9bab1..4191a96 100644 --- a/src/main/java/org/patinanetwork/patchats/api/auth/security/SecurityConfig.java +++ b/src/main/java/org/patinanetwork/patchats/api/auth/security/SecurityConfig.java @@ -11,7 +11,7 @@ public class SecurityConfig { /** - * Default/production chain. Sending email is admin-only. + * Default/production chain. Sending email and listing members are admin-only. * *

NOTE: authentication (OAuth2 login / roles) is not yet wired, so this rule currently fails closed — every * caller is denied because no security context is populated. Once the auth domain authenticates admins, the rule @@ -22,6 +22,8 @@ public class SecurityConfig { SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { return http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.POST, "/api/email/**") + .hasRole("ADMIN") + .requestMatchers(HttpMethod.GET, "/api/members") .hasRole("ADMIN") .anyRequest() .permitAll()) diff --git a/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java b/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java index 1d615d6..d2d67b7 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java @@ -1,12 +1,15 @@ package org.patinanetwork.patchats.api.member; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.patinanetwork.patchats.api.member.dto.CreateMemberRequest; import org.patinanetwork.patchats.api.member.dto.MemberDto; import org.patinanetwork.patchats.common.dto.ApiResponder; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,6 +29,13 @@ public ResponseEntity> createMember(@Valid @RequestBody return ResponseEntity.ok(ApiResponder.success("Member created successfully", response)); } + @GetMapping + @Operation(summary = "List all members", description = "Returns every member ordered by creation date.") + public ResponseEntity>> getMembers() { + final List response = memberService.getMembers(); + return ResponseEntity.ok(ApiResponder.success("Members retrieved successfully", response)); + } + // TODO: Implement these endpoints after createMember is fully functional and tested // @PatchMapping("/{id}") // public ResponseEntity> updateMember( diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java index 8532e87..2712802 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java @@ -2,10 +2,12 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.Test; import org.patinanetwork.patchats.api.member.dto.CreateMemberRequest; @@ -62,11 +64,9 @@ void createMemberReturnsOkAndMemberDto() throws Exception { .topics(request.topics()) .extraNotes(request.extraNotes()) .build()); - mockMvc.perform( - post("/api/members") - .contentType(MediaType.APPLICATION_JSON) - .content( - "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"email\":\"john.doe@example.com\",\"linkedInUrl\":\"https://www.linkedin.com/in/johndoe\",\"introduction\":\"Hello, I'm John!\",\"referralSource\":\"Friend\",\"matchPref\":\"Mentor - I am looking for guidance from someone with more experience\",\"industryPref\":\"Technology\",\"rolePref\":\"Software Engineer\",\"topics\":\"College, Career Development\",\"extraNotes\":\"I want to be meet someone in person in NYC\"}")) + mockMvc.perform(post("/api/members") + .contentType(MediaType.APPLICATION_JSON) + .content(memberRequestJson("John"))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.payload.firstName").value(request.firstName())) @@ -86,11 +86,9 @@ void createMemberReturnsOkAndMemberDto() throws Exception { @Test void createMemberReturnsBadRequestWhenFirstNameIsBlank() throws Exception { - mockMvc.perform( - post("/api/members") - .contentType(MediaType.APPLICATION_JSON) - .content( - "{\"firstName\":\"\",\"lastName\":\"Doe\",\"email\":\"john.doe@example.com\",\"linkedInUrl\":\"https://www.linkedin.com/in/johndoe\",\"introduction\":\"Hello, I'm John!\",\"referralSource\":\"Friend\",\"matchPref\":\"Mentor - I am looking for guidance from someone with more experience\",\"industryPref\":\"Technology\",\"rolePref\":\"Software Engineer\",\"topics\":\"College, Career Development\",\"extraNotes\":\"I want to be meet someone in person in NYC\"}")) + mockMvc.perform(post("/api/members") + .contentType(MediaType.APPLICATION_JSON) + .content(memberRequestJson(""))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.success").value(false)); } @@ -98,12 +96,55 @@ void createMemberReturnsBadRequestWhenFirstNameIsBlank() throws Exception { @Test void createMemberReturnsConflictOnDuplicateEmail() throws Exception { when(memberService.createMember(any())).thenThrow(new MemberDuplicateException("john.doe@example.com")); - mockMvc.perform( - post("/api/members") - .contentType(MediaType.APPLICATION_JSON) - .content( - "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"email\":\"john.doe@example.com\",\"linkedInUrl\":\"https://www.linkedin.com/in/johndoe\",\"introduction\":\"Hello, I'm John!\",\"referralSource\":\"Friend\",\"matchPref\":\"Mentor - I am looking for guidance from someone with more experience\",\"industryPref\":\"Technology\",\"rolePref\":\"Software Engineer\",\"topics\":\"College, Career Development\",\"extraNotes\":\"I want to be meet someone in person in NYC\"}")) + mockMvc.perform(post("/api/members") + .contentType(MediaType.APPLICATION_JSON) + .content(memberRequestJson("John"))) .andExpect(status().isConflict()) .andExpect(jsonPath("$.success").value(false)); } + + @Test + void getMembersReturnsAllMembers() throws Exception { + final MemberDto firstMember = MemberDto.builder() + .id(UUID.randomUUID()) + .firstName("John") + .lastName("Doe") + .email("john.doe@example.com") + .active(true) + .build(); + final MemberDto secondMember = MemberDto.builder() + .id(UUID.randomUUID()) + .firstName("Jane") + .lastName("Doe") + .email("jane.doe@example.com") + .active(false) + .build(); + when(memberService.getMembers()).thenReturn(List.of(firstMember, secondMember)); + + mockMvc.perform(get("/api/members")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.payload.length()").value(2)) + .andExpect(jsonPath("$.payload[0].firstName").value("John")) + .andExpect(jsonPath("$.payload[0].lastName").value("Doe")) + .andExpect(jsonPath("$.payload[1].email").value("jane.doe@example.com")); + } + + private static String memberRequestJson(String firstName) { + return """ + { + "firstName": "%s", + "lastName": "Doe", + "email": "john.doe@example.com", + "linkedInUrl": "https://www.linkedin.com/in/johndoe", + "introduction": "Hello, I'm John!", + "referralSource": "Friend", + "matchPref": "Mentor - I am looking for guidance from someone with more experience", + "industryPref": "Technology", + "rolePref": "Software Engineer", + "topics": "College, Career Development", + "extraNotes": "I want to be meet someone in person in NYC" + } + """.formatted(firstName); + } } diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java index 8bf3318..d99e679 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -113,7 +114,7 @@ void createMemberThrowsExceptionWhenEmailAlreadyExists() { } @Test - void updateMember_throwsExceptionWhenMemberNotFound() { + void updateMemberThrowsExceptionWhenMemberNotFound() { final UUID id = UUID.randomUUID(); final UpdateMemberRequest request = new UpdateMemberRequest( "UpdatedFirstName", @@ -134,7 +135,7 @@ void updateMember_throwsExceptionWhenMemberNotFound() { } @Test - void updateMember_successWithOnlyNameField() { + void updateMemberSuccessWithOnlyNameField() { final UUID id = UUID.randomUUID(); final UpdateMemberRequest request = new UpdateMemberRequest("UpdatedFirstName", null, null, null, null, null, null, null, null, null); @@ -179,7 +180,7 @@ void updateMember_successWithOnlyNameField() { } @Test - void updateMember_successWithAllNullFields() { + void updateMemberSuccessWithAllNullFields() { final UUID id = UUID.randomUUID(); final UpdateMemberRequest request = new UpdateMemberRequest(null, null, null, null, null, null, null, null, null, null); @@ -211,7 +212,7 @@ void updateMember_successWithAllNullFields() { } @Test - void updateMember_successWithAllFields() { + void updateMemberSuccessWithAllFields() { final UUID id = UUID.randomUUID(); final UpdateMemberRequest request = new UpdateMemberRequest( "UpdatedFirstName", @@ -269,4 +270,31 @@ void updateMember_successWithAllFields() { assertEquals(request.topics(), response.getTopics()); assertEquals(request.extraNotes(), response.getExtraNotes()); } + + @Test + void getMembersReturnsEveryMemberAsDto() { + final Member firstMember = Member.builder() + .id(UUID.randomUUID()) + .firstName("John") + .lastName("Doe") + .email("john.doe@example.com") + .active(true) + .build(); + final Member secondMember = Member.builder() + .id(UUID.randomUUID()) + .firstName("Jane") + .lastName("Doe") + .email("jane.doe@example.com") + .active(false) + .build(); + when(memberRepo.getMembers()).thenReturn(List.of(firstMember, secondMember)); + + final List response = memberService.getMembers(); + + assertEquals(2, response.size()); + assertEquals(firstMember.getId(), response.get(0).getId()); + assertEquals("John", response.get(0).getFirstName()); + assertEquals("Doe", response.get(0).getLastName()); + assertEquals("jane.doe@example.com", response.get(1).getEmail()); + } } From b7d3b935650d490e0b1ce562ec0c9aaf5901835b Mon Sep 17 00:00:00 2001 From: Arshadul Monir Date: Mon, 10 Aug 2026 14:53:26 -0400 Subject: [PATCH 2/2] Added Mock data to display users in dev environment --- .github/scripts/src/db/migrate.ts | 2 +- Justfile | 4 +- db/README.md | 11 +- .../R__Mock_V0001_Insert_test_members.sql | 167 ++++++++++++++++++ 4 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 db/repeated/R__Mock_V0001_Insert_test_members.sql diff --git a/.github/scripts/src/db/migrate.ts b/.github/scripts/src/db/migrate.ts index a325560..41dc5e5 100644 --- a/.github/scripts/src/db/migrate.ts +++ b/.github/scripts/src/db/migrate.ts @@ -26,7 +26,7 @@ async function main() { DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, - })`./mvnw flyway:migrate -Dflyway.locations=filesystem:db`; + })`./mvnw flyway:migrate -Dflyway.locations=filesystem:db/migration`; } function parseCiEnv(environment: string) { diff --git a/Justfile b/Justfile index b29dda3..bcf7969 100644 --- a/Justfile +++ b/Justfile @@ -2,11 +2,11 @@ set shell := ["bash", "-uc"] # Migrate to local DB migrate *args: - dotenvx run -- ./mvnw flyway:migrate -Dflyway.locations=filesystem:./db {{args}} + dotenvx run -- ./mvnw flyway:migrate -Dflyway.locations=filesystem:./db/migration,filesystem:./db/repeated {{args}} # Drop local DB drop *args: - dotenvx run -- ./mvnw flyway:clean -Dflyway.locations=filesystem:./db -Dflyway.cleanDisabled=false {{args}} + dotenvx run -- ./mvnw flyway:clean -Dflyway.locations=filesystem:./db/migration,filesystem:./db/repeated -Dflyway.cleanDisabled=false {{args}} # Run the backend Spring server backend-dev *args: diff --git a/db/README.md b/db/README.md index 3ce1804..79f66cf 100644 --- a/db/README.md +++ b/db/README.md @@ -100,7 +100,7 @@ Versioned migrations are under `db/migration/` - Versioned migrations will only run once. - You can use these migrations to define tables, schemas, columns (otherwise known as DDL) OR define insertions/updates/deletes of certain columns (otherwise known as DML). -> If you need to generate mock data **that does not need to be in production**, please look at the docs on [repeatable migrations](#repeatable-migrations) +> If you need mock data **only for local development**, see [local repeatable migrations](#local-repeatable-migrations). ### Naming Scheme @@ -123,18 +123,17 @@ V0642__Insert_new_tag_enums.SQL V9999__Delete_user_table.SQL ``` -## Repeatable Migrations +## Local Repeatable Migrations -Repeated migrations are under `db/repeated/` +Local repeatable migrations are under `db/repeated/`. > [!NOTE] -> Repeatable migrations are only applied to local & CI databases.
-> Repeatable migrations are **NOT** applied to the production & staging database. +> `just migrate` explicitly includes this directory. Production and staging migrations only scan `db/migration`, so local seed data is never applied to remote databases. ### Explanation 1. Instead of being run just once, repeatable migrations are (re-)applied to a database on [migrate](https://documentation.red-gate.com/fd/migrate-277578887.html) every time their checksum changes. -1. Our main use for repeatable migrations are to generate mock data to use locally and in our CI database, but **is not needed for our production or staging database**. +1. These migrations generate mock data for local development and are excluded from production and staging migrations. #### Requirements diff --git a/db/repeated/R__Mock_V0001_Insert_test_members.sql b/db/repeated/R__Mock_V0001_Insert_test_members.sql new file mode 100644 index 0000000..ccc5a52 --- /dev/null +++ b/db/repeated/R__Mock_V0001_Insert_test_members.sql @@ -0,0 +1,167 @@ +INSERT INTO members ( + id, + first_name, + last_name, + email, + linked_in_url, + introduction, + referral_source, + active, + match_pref, + industry_pref, + role_pref, + topics, + extra_notes +) +VALUES + ( + '10000000-0000-4000-8000-000000000001', + 'Avery', + 'Chen', + 'avery.chen@example.com', + 'https://www.linkedin.com/in/avery-chen-example', + 'Product designer interested in making financial tools easier to use.', + 'Patina Network newsletter', + TRUE, + 'Peer - I want to meet someone with similar experience', + 'Financial Technology', + 'Product Designer', + 'Design systems, Financial literacy, Career growth', + 'Available for coffee in Manhattan or a virtual chat.' + ), + ( + '10000000-0000-4000-8000-000000000002', + 'Jordan', + 'Williams', + 'jordan.williams@example.com', + 'https://www.linkedin.com/in/jordan-williams-example', + 'Backend engineer building reliable systems for healthcare teams.', + 'Friend', + TRUE, + 'Mentor - I am looking for guidance from someone with more experience', + 'Healthcare Technology', + 'Software Engineer', + 'Distributed systems, Leadership, Open source', + 'Usually available after work on Tuesdays and Thursdays.' + ), + ( + '10000000-0000-4000-8000-000000000003', + 'Priya', + 'Patel', + 'priya.patel@example.com', + 'https://www.linkedin.com/in/priya-patel-example', + 'Founder working on software that helps small businesses measure emissions.', + 'Patina Network event', + TRUE, + 'Peer - I want to exchange ideas with another builder', + 'Climate Technology', + 'Founder', + 'Fundraising, Sustainability, Product strategy', + 'Happy to meet virtually or in Brooklyn.' + ), + ( + '10000000-0000-4000-8000-000000000004', + 'Marcus', + 'Johnson', + 'marcus.johnson@example.com', + NULL, + 'Data analyst focused on improving outcomes for public school students.', + 'Colleague', + FALSE, + 'Mentor - I can offer guidance to someone earlier in their career', + 'Education', + 'Data Analyst', + 'Analytics, Public policy, Mentorship', + 'Taking a break from matching this month.' + ), + ( + '10000000-0000-4000-8000-000000000005', + 'Sofia', + 'Rodriguez', + 'sofia.rodriguez@example.com', + 'https://www.linkedin.com/in/sofia-rodriguez-example', + 'Community program director connecting volunteers with neighborhood nonprofits.', + 'Community partner', + TRUE, + 'Open - I am happy to meet a peer, mentor, or mentee', + 'Nonprofit', + 'Program Director', + 'Community building, Social impact, Volunteer engagement', + 'Prefers an in-person conversation when possible.' + ), + ( + '10000000-0000-4000-8000-000000000006', + 'Ethan', + 'Kim', + 'ethan.kim@example.com', + 'https://www.linkedin.com/in/ethan-kim-example', + 'Graduate student researching responsible machine learning applications.', + 'University alumni group', + TRUE, + 'Mentor - I am looking for guidance from someone with more experience', + 'Artificial Intelligence', + 'Graduate Researcher', + 'Responsible AI, Research careers, Entrepreneurship', + NULL + ), + ( + '10000000-0000-4000-8000-000000000007', + 'Nia', + 'Thompson', + 'nia.thompson@example.com', + NULL, + 'Marketing strategist helping independent creators grow sustainable audiences.', + 'LinkedIn', + TRUE, + 'Peer - I want to meet someone with similar experience', + 'Media', + 'Marketing Strategist', + 'Brand strategy, Creator economy, Storytelling', + 'Virtual chats work best during weekday mornings.' + ), + ( + '10000000-0000-4000-8000-000000000008', + 'Leo', + 'Martinez', + 'leo.martinez@example.com', + 'https://www.linkedin.com/in/leo-martinez-example', + 'Operations manager interested in technology-enabled hospitality experiences.', + 'Friend', + TRUE, + 'Open - I am happy to meet a peer, mentor, or mentee', + 'Hospitality', + 'Operations Manager', + 'Operations, Customer experience, Small business', + 'Available in Queens or by video call.' + ), + ( + '10000000-0000-4000-8000-000000000009', + 'Maya', + 'Brooks', + 'maya.brooks@example.com', + 'https://www.linkedin.com/in/maya-brooks-example', + 'People operations leader building inclusive teams at growing companies.', + 'Patina Network event', + TRUE, + 'Mentor - I can offer guidance to someone earlier in their career', + 'Professional Services', + 'People Operations Director', + 'Inclusive hiring, Management, Organizational design', + 'Open to meeting anywhere near a major subway line.' + ), + ( + '10000000-0000-4000-8000-000000000010', + 'Noah', + 'Okafor', + 'noah.okafor@example.com', + NULL, + 'Independent consultant helping local retailers modernize their operations.', + 'Community partner', + FALSE, + 'Peer - I want to exchange ideas with another builder', + 'Retail', + 'Business Consultant', + 'Digital transformation, Consulting, Local business', + 'Inactive seed record for testing member status filters.' + ) +ON CONFLICT (email) DO NOTHING;