Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/src/db/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 5 additions & 6 deletions db/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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. <br />
> 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

Expand Down
167 changes: 167 additions & 0 deletions db/repeated/R__Mock_V0001_Insert_test_members.sql
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions js/src/app/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -41,6 +42,7 @@ export const router = createBrowserRouter([
element: <AdminLayout />,
children: [
{ path: "admin", element: <AdminPage /> },
{ path: "admin/members", element: <MembersPage /> },
{ path: "admin/email", element: <EmailAdminPage /> },
{ path: "sample/admin", element: <SampleAdminPage /> },
],
Expand Down
32 changes: 32 additions & 0 deletions js/src/features/members/Members.page.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<MembersPage />);

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(<MembersPage />);

expect(await screen.findByText("No members found.")).toBeInTheDocument();
expect(screen.getByText("0 members")).toBeInTheDocument();
});
114 changes: 114 additions & 0 deletions js/src/features/members/Members.page.tsx
Original file line number Diff line number Diff line change
@@ -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() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No default exports pls

const { data: members, isError, isPending } = useMembers();

if (isPending) {
return (
<Center py="xl">
<Loader aria-label="Loading members" />
</Center>
);
}

if (isError) {
return (
<Alert color="red" title="Members could not be loaded">
Try refreshing the page. If the problem continues, check that the
backend is running.
</Alert>
);
}

return (
<Stack>
<Group justify="space-between">
<div>
<Title order={2}>Members</Title>
<Text c="dimmed">Everyone registered for PatChats.</Text>
</div>
<Badge size="lg" variant="light">
{members.length} {members.length === 1 ? "member" : "members"}
</Badge>
</Group>
{members.length === 0 ?
<Paper p="xl" withBorder>
<Text c="dimmed" ta="center">
No members found.
</Text>
</Paper>
: <Table.ScrollContainer minWidth={1100}>
<Table highlightOnHover striped withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Email</Table.Th>
<Table.Th>LinkedIn</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Match preference</Table.Th>
<Table.Th>Industry</Table.Th>
<Table.Th>Role</Table.Th>
<Table.Th>Topics</Table.Th>
<Table.Th>Joined</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{members.map((member) => (
<Table.Tr key={member.id}>
<Table.Td fw={600}>
{member.firstName} {member.lastName}
</Table.Td>
<Table.Td>
<Anchor href={`mailto:${member.email}`}>
{member.email}
</Anchor>
</Table.Td>
<Table.Td>
{member.linkedInUrl ?
<Anchor
href={member.linkedInUrl}
rel="noreferrer"
target="_blank"
>
Profile
</Anchor>
: "—"}
</Table.Td>
<Table.Td>
<Badge color={member.active ? "green" : "gray"}>
{member.active ? "Active" : "Inactive"}
</Badge>
</Table.Td>
<Table.Td>{member.matchPref ?? "—"}</Table.Td>
<Table.Td>{member.industryPref ?? "—"}</Table.Td>
<Table.Td>{member.rolePref ?? "—"}</Table.Td>
<Table.Td>{member.topics ?? "—"}</Table.Td>
<Table.Td>
{dateFormatter.format(new Date(member.createdAt))}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
}
</Stack>
);
}
Loading
Loading