Skip to content

Commit 41d0ffe

Browse files
committed
Reuse /me email renderer on member profiles
1 parent 61c1c54 commit 41d0ffe

File tree

4 files changed

+110
-133
lines changed

4 files changed

+110
-133
lines changed

src/queries/me/render.ts

Lines changed: 15 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {getGravatarThumbnail} from '../../templates/avatar';
2-
import {Html, html, joinHtml, sanitizeOption, sanitizeString} from '../../types/html';
2+
import {html, sanitizeOption, sanitizeString} from '../../types/html';
33
import {ViewModel} from './view-model';
44
import {renderMemberNumber} from '../../templates/member-number';
55
import {renderOwnerAgreementStatus} from '../shared-render/owner-agreement';
@@ -11,9 +11,10 @@ import {howToGetTrained} from '../shared-render/training-status';
1111
import {ownerResources} from './owner-resources';
1212
import { renderTrainingMatrix } from '../training-matrix/render';
1313
import * as O from 'fp-ts/Option';
14-
import { MemberEmail } from '../../read-models/shared-state/return-types';
15-
import { EmailAddress } from '../../types';
16-
import { SEND_EMAIL_VERIFICATION_COOLDOWN_MS } from '../../commands/members/email-state';
14+
import {MemberEmail} from '../../read-models/shared-state/return-types';
15+
import {EmailAddress} from '../../types';
16+
import {SEND_EMAIL_VERIFICATION_COOLDOWN_MS} from '../../commands/members/email-state';
17+
import {renderMemberEmails} from '../shared-render/member-emails';
1718

1819
const editFormOfAddress = (viewModel: ViewModel) => html`
1920
<a
@@ -59,68 +60,16 @@ const addEmail = (memberNumber: number) => html`
5960
const editAvatar = () =>
6061
html`<a href="https://gravatar.com/profile">Edit via Gravatar</a>`;
6162

62-
const sortMemberEmailByVerifiedThenAddedDate = (a: MemberEmail, b: MemberEmail): 1 | -1 | 0 => {
63-
if (O.isSome(a.verifiedAt) && O.isSome(b.verifiedAt)) {
64-
const aVerifiedTimestamp = a.verifiedAt.value.getTime();
65-
const bVerifiedTimestamp = b.verifiedAt.value.getTime();
66-
if (aVerifiedTimestamp > bVerifiedTimestamp) {
67-
return 1;
68-
}
69-
if (aVerifiedTimestamp === bVerifiedTimestamp) {
70-
return 0;
71-
}
72-
return -1;
73-
}
74-
if (O.isSome(a.verifiedAt) && O.isNone(b.verifiedAt)) {
75-
return 1;
76-
}
77-
if (O.isSome(b.verifiedAt) && O.isNone(a.verifiedAt)) {
78-
return -1;
79-
}
80-
const aAddedTimestamp = a.addedAt.getTime();
81-
const bAddedTimestamp = b.addedAt.getTime();
82-
if (aAddedTimestamp > bAddedTimestamp) {
83-
return 1;
84-
}
85-
if (aAddedTimestamp === bAddedTimestamp) {
86-
return 0;
87-
}
88-
return -1;
89-
};
90-
91-
const renderEmailAddresses = (viewModel: ViewModel) => {
92-
const emails = viewModel.member.emails.filter(
93-
email => email.emailAddress != viewModel.member.primaryEmailAddress
94-
).toSorted(sortMemberEmailByVerifiedThenAddedDate);
95-
96-
const renderEmailTableRow = (email: MemberEmail): Html => {
97-
return html`
98-
<tr>
99-
<td></td>
100-
<td>${sanitizeString(email.emailAddress)}${O.isSome(email.verifiedAt) ? html`✅` : html``}</td>
101-
<td>${
102-
O.isSome(email.verifiedAt)
103-
? setPrimaryEmail(email.emailAddress, viewModel.member.memberNumber)
104-
: sendVerifyEmail(viewModel.member.memberNumber, email)
105-
}</td>
106-
</tr>
107-
`;
108-
};
109-
110-
return html`<table>
111-
<tr>
112-
<td>Primary</td>
113-
<td>${sanitizeString(viewModel.member.primaryEmailAddress)}</td>
114-
<td></td>
115-
</tr>
116-
${joinHtml(emails.toSorted(sortMemberEmailByVerifiedThenAddedDate).map(renderEmailTableRow))}
117-
<tr>
118-
<td colspan="3">
119-
${addEmail(viewModel.member.memberNumber)}
120-
</td>
121-
</tr>
122-
</table>`;
123-
};
63+
const renderEmailAddresses = (viewModel: ViewModel) =>
64+
renderMemberEmails({
65+
primaryEmailAddress: viewModel.member.primaryEmailAddress,
66+
emails: viewModel.member.emails,
67+
renderAction: email =>
68+
O.isSome(email.verifiedAt)
69+
? setPrimaryEmail(email.emailAddress, viewModel.member.memberNumber)
70+
: sendVerifyEmail(viewModel.member.memberNumber, email),
71+
addEmailAction: O.some(addEmail(viewModel.member.memberNumber)),
72+
});
12473

12574
const renderMemberDetails = (viewModel: ViewModel) => html`
12675
<table>

src/queries/member/render.ts

Lines changed: 11 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as O from 'fp-ts/Option';
22
import {getGravatarProfile, getGravatarThumbnail} from '../../templates/avatar';
3-
import {Html, html, joinHtml, sanitizeOption, sanitizeString} from '../../types/html';
3+
import {Html, html, sanitizeOption} from '../../types/html';
44
import {ViewModel} from './view-model';
55
import {
66
renderMemberNumber,
@@ -10,7 +10,7 @@ import {memberStatusTag} from '../../templates/member-status';
1010
import {otherMemberNumbersTooltip} from '../shared-render/other-member-numbers-tooltip';
1111
import {renderTrainingMatrix} from '../training-matrix/render';
1212
import {renderOwnerAgreementStatus} from '../shared-render/owner-agreement';
13-
import {MemberEmail} from '../../read-models/shared-state/return-types';
13+
import {renderMemberEmails} from '../shared-render/member-emails';
1414

1515
const ownPageBanner = html`<h1>This is your profile!</h1>`;
1616

@@ -31,72 +31,20 @@ const editAvatar = () =>
3131

3232
const addEmail = (memberNumber: number) => html`
3333
<a href="/members/add-email?member=${memberNumber}">
34-
Add email
34+
Add New Email
3535
</a>
3636
`;
3737

38-
const renderEmailStatus = (
39-
email: MemberEmail,
40-
primaryEmailAddress: string
41-
): string => {
42-
if (email.emailAddress === primaryEmailAddress) {
43-
return 'Primary';
44-
}
45-
if (O.isSome(email.verifiedAt)) {
46-
return 'Verified';
47-
}
48-
return 'Unverified';
49-
};
50-
51-
const renderEmails = (viewModel: ViewModel): Html => {
52-
const emails = [...viewModel.member.emails].sort((a, b) => {
53-
const aIsPrimary = a.emailAddress === viewModel.member.primaryEmailAddress;
54-
const bIsPrimary = b.emailAddress === viewModel.member.primaryEmailAddress;
55-
if (aIsPrimary && !bIsPrimary) {
56-
return -1;
57-
}
58-
if (!aIsPrimary && bIsPrimary) {
59-
return 1;
60-
}
61-
return a.emailAddress.localeCompare(b.emailAddress);
38+
const renderEmails = (viewModel: ViewModel) =>
39+
renderMemberEmails({
40+
primaryEmailAddress: viewModel.member.primaryEmailAddress,
41+
emails: viewModel.member.emails,
42+
renderAction: () => html``,
43+
addEmailAction: viewModel.isSuperUser
44+
? O.some(addEmail(viewModel.member.memberNumber))
45+
: O.none,
6246
});
6347

64-
return html`
65-
<div>
66-
<table>
67-
<thead>
68-
<tr>
69-
<th scope="col">Email</th>
70-
<th scope="col">Status</th>
71-
</tr>
72-
</thead>
73-
<tbody>
74-
${joinHtml(
75-
emails.map(
76-
email => html`
77-
<tr>
78-
<td>${sanitizeString(email.emailAddress)}</td>
79-
<td>
80-
${sanitizeString(
81-
renderEmailStatus(
82-
email,
83-
viewModel.member.primaryEmailAddress
84-
)
85-
)}
86-
</td>
87-
</tr>
88-
`
89-
)
90-
)}
91-
</tbody>
92-
</table>
93-
${viewModel.isSuperUser
94-
? html`<p>${addEmail(viewModel.member.memberNumber)}</p>`
95-
: html``}
96-
</div>
97-
`;
98-
};
99-
10048
const ifSelf = (viewModel: ViewModel, fragment: Html) =>
10149
viewModel.isSelf ? fragment : '';
10250

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as O from 'fp-ts/Option';
2+
import {MemberEmail} from '../../read-models/shared-state/return-types';
3+
import {Html, html, joinHtml, sanitizeString} from '../../types/html';
4+
5+
const sortMemberEmailByVerifiedThenAddedDate = (
6+
a: MemberEmail,
7+
b: MemberEmail
8+
): 1 | -1 | 0 => {
9+
if (O.isSome(a.verifiedAt) && O.isSome(b.verifiedAt)) {
10+
const aVerifiedTimestamp = a.verifiedAt.value.getTime();
11+
const bVerifiedTimestamp = b.verifiedAt.value.getTime();
12+
if (aVerifiedTimestamp > bVerifiedTimestamp) {
13+
return 1;
14+
}
15+
if (aVerifiedTimestamp === bVerifiedTimestamp) {
16+
return 0;
17+
}
18+
return -1;
19+
}
20+
if (O.isSome(a.verifiedAt) && O.isNone(b.verifiedAt)) {
21+
return 1;
22+
}
23+
if (O.isSome(b.verifiedAt) && O.isNone(a.verifiedAt)) {
24+
return -1;
25+
}
26+
const aAddedTimestamp = a.addedAt.getTime();
27+
const bAddedTimestamp = b.addedAt.getTime();
28+
if (aAddedTimestamp > bAddedTimestamp) {
29+
return 1;
30+
}
31+
if (aAddedTimestamp === bAddedTimestamp) {
32+
return 0;
33+
}
34+
return -1;
35+
};
36+
37+
type RenderMemberEmailsArgs = {
38+
addEmailAction: O.Option<Html>;
39+
emails: ReadonlyArray<MemberEmail>;
40+
primaryEmailAddress: string;
41+
renderAction: (email: MemberEmail) => Html;
42+
};
43+
44+
export const renderMemberEmails = ({
45+
addEmailAction,
46+
emails,
47+
primaryEmailAddress,
48+
renderAction,
49+
}: RenderMemberEmailsArgs): Html => {
50+
const secondaryEmails = emails
51+
.filter(email => email.emailAddress !== primaryEmailAddress)
52+
.toSorted(sortMemberEmailByVerifiedThenAddedDate);
53+
54+
const renderEmailTableRow = (email: MemberEmail): Html => html`
55+
<tr>
56+
<td></td>
57+
<td>
58+
${sanitizeString(email.emailAddress)}
59+
${O.isSome(email.verifiedAt) ? html`✅` : html``}
60+
</td>
61+
<td>${renderAction(email)}</td>
62+
</tr>
63+
`;
64+
65+
return html`
66+
<table>
67+
<tr>
68+
<td>Primary</td>
69+
<td>${sanitizeString(primaryEmailAddress)}</td>
70+
<td></td>
71+
</tr>
72+
${joinHtml(secondaryEmails.map(renderEmailTableRow))}
73+
${O.isSome(addEmailAction)
74+
? html`<tr>
75+
<td colspan="3">${addEmailAction.value}</td>
76+
</tr>`
77+
: html``}
78+
</table>
79+
`;
80+
};

tests/queries/member/render.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ const renderPage = (viewModel: ViewModel) => {
6565
};
6666

6767
describe('member render', () => {
68-
it('shows the full email list with primary and verification statuses', () => {
68+
it('shows the same email table shape used on /me', () => {
6969
const page = renderPage(buildViewModel(true));
7070

7171
expect(page.textContent).toContain('Email addresses');
7272
expect(page.textContent).toContain(primaryEmail);
7373
expect(page.textContent).toContain(verifiedSecondaryEmail);
7474
expect(page.textContent).toContain(unverifiedEmail);
7575
expect(page.textContent).toContain('Primary');
76-
expect(page.textContent).toContain('Verified');
77-
expect(page.textContent).toContain('Unverified');
76+
expect(page.textContent).not.toContain('Send Verification Email');
77+
expect(page.textContent).not.toContain('Make Primary Email');
7878
});
7979

8080
it('shows the add email action to super users', () => {
@@ -83,7 +83,7 @@ describe('member render', () => {
8383
'a[href="/members/add-email?member=123"]'
8484
);
8585

86-
expect(addEmailLink?.textContent).toContain('Add email');
86+
expect(addEmailLink?.textContent).toContain('Add New Email');
8787
});
8888

8989
it('does not show the add email action to non-super users', () => {

0 commit comments

Comments
 (0)