Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add startups sidebar to partners #2688

Merged
merged 5 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion src/app/pages/partners/active-filters/active-filters.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

.active-filters {
@extend %content;
padding: 0;

@include wider-than($phone-max) {
margin-top: 4rem;
Expand Down
70 changes: 69 additions & 1 deletion src/app/pages/partners/results/results.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

.partners .results {
align-items: start;
padding: 2rem 0 20rem;
padding: 2rem $normal-margin 20rem;
row-gap: 2rem;

@include width-up-to($phone-max) {
Expand Down Expand Up @@ -104,4 +104,72 @@
}
}
}

.boxed {
grid-gap: 3rem;

@include width-up-to($phone-max) {
grid-gap: $normal-margin;
}
}

.with-sidebar {
h2 {
text-align: center;
}

@include width-up-to($phone-max) {
> .boxed {
padding: 0;
}

.sidebar-content {
display: flex;
flex-direction: column;
grid-gap: 3rem;
margin-top: 3rem;
}
}

@include wider-than($phone-max) {
display: flex;
flex-direction: row;
gap: 3rem;
max-width: 120rem;
margin: 0 auto;
padding-right: $normal-margin;

.grid {
max-width: unset;
}

> .sidebar {
min-width: 20rem;
border: thin solid black;
height: max-content;

.sidebar-content {
background-color: ui-color(white);
}

h2 {
@include set-font(h3);

background-color: os-color(gray);
color: ui-color(white);
padding: $normal-margin;
}

.grid {
gap: 0.2rem;
}

.card {
border-radius: 0;
box-shadow: none;
border-bottom: thin solid ui-color(form-border);
}
}
}
}
}
138 changes: 103 additions & 35 deletions src/app/pages/partners/results/results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {useDataFromPromise} from '~/helpers/page-data-utils';
import SelectedPartnerDialog from './selected-partner-dialog';
import shuffle from 'lodash/shuffle';
import orderBy from 'lodash/orderBy';
import './results.scss';
import partition from 'lodash/partition';
import {differenceInYears} from 'date-fns';
import './results.scss';

export const costOptions = ['$0 - $10', '$11 - $25', '$26 - $40', '> $40'].map(
(label) => ({
Expand Down Expand Up @@ -112,10 +113,7 @@ function filterBy(
// eslint-disable-next-line complexity
function useFilteredEntries(entries: PartnerEntry[]) {
const {books, types, advanced, sort, resultCount} = useSearchContext();
const unfilteredResults = React.useMemo(
() => shuffle(entries),
[entries]
);
const unfilteredResults = React.useMemo(() => shuffle(entries), [entries]);
const finalResult = React.useMemo(() => {
let result = filterByBooks(unfilteredResults, books);

Expand Down Expand Up @@ -160,8 +158,8 @@ function useFilteredEntries(entries: PartnerEntry[]) {
}

function advancedFilterKeys(partnerEntry: PartnerData) {
return (Object.keys(partnerEntry) as Array<keyof PartnerData>).filter(
(k) => ([false, true] as unknown[]).includes(partnerEntry[k])
return (Object.keys(partnerEntry) as Array<keyof PartnerData>).filter((k) =>
([false, true] as unknown[]).includes(partnerEntry[k])
);
}

Expand Down Expand Up @@ -202,13 +200,51 @@ function resultEntry(pd: PartnerData) {
cost: pd.affordability_cost,
rating: pd.average_rating.rating__avg,
ratingCount: pd.rating_count,
partnershipLevel: pd.partnership_level,
partnershipLevel: pd.partnership_level as string,
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made startups display as a normal grid-group if it's there is only one other grid-group.
I think I've got the spacing all fixed up.
The "as string" was leftover from when I had it defined as string | null. Good catch, thanks.
I removed some padding on the mobile-controls row, which was enough to make it good down to 320px screen width.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah this is a lot better, nice idea to switch up the display based on the available groups.

yearsAsPartner: pd.partner_anniversary_date
? differenceInYears(Date.now(), new Date(pd.partner_anniversary_date))
? differenceInYears(
Date.now(),
new Date(pd.partner_anniversary_date)
)
: null
};
}

function Sidebar({entries}: {entries: PartnerEntry[]}) {
return (
<div className="sidebar">
<div className="sidebar-content">
<h2>Startups</h2>
<ResultGrid entries={entries} />
</div>
</div>
);
}

const headings: Record<Ages, string> = {
'10': '10+ years as Technology Partners',
'7': '7-10 years as partners',
'4': '4-7 years as partners',
'1': '1-3 years as partners',
new: 'New partners'
};
const ages: Ages[] = ['10', '7', '4', '1', 'new'];

function HeadingAndResultGrid({
age,
entries
}: {
age: Ages;
entries: PartnerEntry[];
}) {
return (
<React.Fragment>
<h2>{headings[age as Ages]}</h2>
<ResultGrid entries={entries} />
</React.Fragment>
);
}

type Ages = '10' | '7' | '4' | '1' | 'new';

function ResultGridLoader({
Expand All @@ -218,20 +254,27 @@ function ResultGridLoader({
partnerData: PartnerData[];
linkTexts: LinkTexts;
}) {
// // *** FOR TESTING because Dev data is missing some things
// let altered = false;

// if (!altered) {
// partnerData.slice(-5).forEach((d) => {d.partnership_level = 'startup'});
// partnerData.slice(0, 5).forEach((d, i) => {d.partner_anniversary_date = `10 Jun ${2014 + i}`});
// altered = true;
// }
// // *** /FOR TESTING
const entries = React.useMemo(
() => partnerData.map(resultEntry),
() => partnerData
.filter((d) => d.partnership_level !== null)
.map(resultEntry),
[partnerData]
);
const filteredEntries = useFilteredEntries(entries);
const ages: Ages[] = ['10', '7', '4', '1'];
const headings: Record<Ages, string> = {
'10': '10+ years as Technology Partners',
'7': '7-10 years as partners',
'4': '4-7 years as partners',
'1': '1-3 years as partners',
new: 'New partners'
};
const partnersByAge = filteredEntries.reduce((a, b) => {
const [startups, nonStartups] = partition(
filteredEntries,
(e) => e.partnershipLevel?.toLowerCase() === 'startup'
);
const partnersByAge = nonStartups.reduce((a, b) => {
const bucket =
ages.find((age) => (b.yearsAsPartner ?? 0) >= Number(age)) ?? 'new';

Expand All @@ -241,22 +284,52 @@ function ResultGridLoader({
a[bucket].push(b);
return a;
}, {} as Record<string, PartnerEntry[]>);
const foundAges = ages.filter((a) => partnersByAge[a]);

if (startups.length > 0) {
const [firstAge, ...otherAges] = foundAges;

return (
<section className="results">
<div className="boxed">
<HeadingAndResultGrid
age={firstAge}
entries={partnersByAge[firstAge]}
/>
</div>
<SelectedPartnerDialog
linkTexts={linkTexts}
entries={filteredEntries}
/>
<div className="with-sidebar">
<div className="boxed">
{otherAges.map((age) => (
<HeadingAndResultGrid
key={age}
age={age}
entries={partnersByAge[age]}
/>
))}
</div>
<Sidebar entries={startups} />
</div>
</section>
);
}
return (
<React.Fragment>
{([...ages, 'new'] as Ages[])
.filter((age) => age in partnersByAge)
.map((age) => (
<React.Fragment key={age}>
<h2>{headings[age as Ages]}</h2>
<ResultGrid entries={partnersByAge[age]} />
</React.Fragment>
))}
<section className="results boxed">
{foundAges.map((age) => (
<HeadingAndResultGrid
key={age}
age={age}
entries={partnersByAge[age]}
/>
))}
<SelectedPartnerDialog
linkTexts={linkTexts}
entries={filteredEntries}
/>
</React.Fragment>
</section>
);
}

Expand All @@ -269,14 +342,9 @@ export default function Results({linkTexts}: {linkTexts: LinkTexts}) {
[partnerData]
);


if (!partnerData) {
return null;
}

return (
<section className="results boxed">
<ResultGridLoader {...{partnerData: visiblePartners, linkTexts}} />
</section>
);
return <ResultGridLoader {...{partnerData: visiblePartners, linkTexts}} />;
}
28 changes: 22 additions & 6 deletions test/src/pages/partners/partners.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('partners/results', () => {
});
});

describe('full page', () => {
describe('partners full page', () => {
const user = userEvent.setup();

function Component() {
Expand All @@ -74,17 +74,16 @@ describe('full page', () => {
</ShellContextProvider>
);
}
beforeEach(() => {
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
});

jest.setTimeout(12000);
it('displays grid that filters by type', async () => {
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
const buttons = await screen.findAllByRole('button');

expect(buttons).toHaveLength(6);
await screen.findByText('Carolina Distance Learning');
expect(screen.getAllByRole('link')).toHaveLength(22);
expect(screen.getAllByRole('link')).toHaveLength(21);
await user.click(buttons[1]);
const options = screen.getAllByRole('option');

Expand All @@ -93,6 +92,8 @@ describe('full page', () => {
expect(screen.getAllByRole('link')).toHaveLength(4);
});
it('filters by book', async () => {
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
const bookButton = await screen.findByRole('button', {name: 'Books'});

await user.click(bookButton);
Expand All @@ -106,6 +107,8 @@ describe('full page', () => {
expect(checkboxes).toHaveLength(24);
});
it('filters by advanced filter', async () => {
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
const filterButton = await screen.findByRole('button', {
name: 'Advanced Filters'
});
Expand All @@ -122,6 +125,8 @@ describe('full page', () => {
expect(screen.getAllByRole('link')).toHaveLength(5);
});
it('sorts', async () => {
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
const sortButtons = await screen.findAllByRole('button', {
name: 'Sort'
});
Expand Down Expand Up @@ -170,11 +175,22 @@ describe('full page', () => {
]);
});
it('shows details in dialog', async () => {
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
const partnerLink = await screen.findByRole('link', {name: 'Rice Online Learning'});

await user.click(partnerLink);
screen.getByRole('dialog');
screen.getByText('through the edX platform', {exact: false});
await user.click(screen.getByRole('button', {name: 'close'}));
});
it('displays sidebar of startups', async () => {
sfPartners[0].partnership_level = 'startup'; // eslint-disable-line
mockSfPartners.mockResolvedValue(sfPartners);
render(<Component />);
const startupHeading = await screen.findByRole('heading', {level: 2, name: 'Startups'});

expect(startupHeading.parentNode?.textContent).toContain(sfPartners[0].partner_name);
sfPartners[0].partnership_level = 'Full partner'; // eslint-disable-line
});
});