Skip to content

Commit ac25864

Browse files
authored
Merge pull request #1425 from CodeForAfrica/ft/opportunity_card_list_payloadcms
feat: Add Opportunity Category and List CMS Blocks
2 parents f32f7f7 + 2e16a13 commit ac25864

25 files changed

Lines changed: 764 additions & 45 deletions

apps/trustlab/payload.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Reports,
1818
Playbooks,
1919
Toolkits,
20+
Opportunities,
2021
} from "@/trustlab/payload/collections";
2122
import plugins from "@/trustlab/payload/plugins";
2223
import SiteSettings from "@/trustlab/payload/globals";
@@ -82,6 +83,7 @@ export default buildConfig({
8283
// Group: Settings
8384
Reports,
8485
Users,
86+
Opportunities,
8587
] as CollectionConfig[],
8688
cors,
8789
csrf,
85.4 KB
Loading
46.8 KB
Loading
111 KB
Loading

apps/trustlab/src/components/Filters/Filters.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const Filters = React.forwardRef(function Filters(
6767

6868
// Auto-generate options for known types if not provided
6969
let resolvedOptions = options;
70-
if (!resolvedOptions) {
70+
if (!resolvedOptions?.length) {
7171
if (type === "year") {
7272
resolvedOptions = generateYearOptions();
7373
} else if (type === "month") {
@@ -175,7 +175,6 @@ const Filters = React.forwardRef(function Filters(
175175
return chips;
176176
}, [selectedValues, getChipLabel]);
177177

178-
console.log("Processed Filters:", processedFilters);
179178
return (
180179
<Box ref={ref} display="flex" flexDirection="column" gap={1} sx={sx}>
181180
{/* Row 1: Filter By Label */}

apps/trustlab/src/components/OpportunityList/OpportunityList.js

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,71 @@ const OpportunityList = forwardRef(function OpportunityList(props, ref) {
2727
...other
2828
} = props;
2929

30-
const [page, setPage] = useState(p?.page);
31-
const [params, setParams] = useState({
32-
type: itemsType,
33-
limit: itemsPerPage,
34-
});
35-
const listRef = useRef(null);
3630
const router = useRouter();
3731
const { query } = router;
38-
const { page: initialPage } = query;
32+
const { page: initialPage, ...queryParams } = query;
3933

40-
useEffect(() => {
34+
const [page, setPage] = useState(() => {
4135
if (initialPage) {
4236
const parsed = parseInt(initialPage, 10);
43-
if (parsed !== page) {
37+
if (!Number.isNaN(parsed)) {
38+
return parsed;
39+
}
40+
}
41+
return p?.page ?? 1;
42+
});
43+
44+
const [params, setParams] = useState(() => ({
45+
type: itemsType,
46+
limit: itemsPerPage,
47+
// Parse query params to restore filter state
48+
...Object.entries(queryParams).reduce((acc, [key, value]) => {
49+
if (typeof value === "string" && value.includes(",")) {
50+
acc[key] = value.split(",");
51+
} else if (value) {
52+
acc[key] = value;
53+
}
54+
return acc;
55+
}, {}),
56+
}));
57+
58+
const listRef = useRef(null);
59+
60+
// Sync page and params with URL query changes
61+
useEffect(() => {
62+
const { page: queryPage, ...currentQueryParams } = query;
63+
64+
// Sync page from URL
65+
if (queryPage) {
66+
const parsed = parseInt(queryPage, 10);
67+
if (!Number.isNaN(parsed) && parsed !== page) {
4468
setPage(parsed);
4569
}
4670
}
71+
72+
// Sync params from URL
73+
const newParams = {
74+
type: itemsType,
75+
limit: itemsPerPage,
76+
...Object.entries(currentQueryParams).reduce((acc, [key, value]) => {
77+
if (typeof value === "string" && value.includes(",")) {
78+
acc[key] = value.split(",");
79+
} else if (value) {
80+
acc[key] = value;
81+
}
82+
return acc;
83+
}, {}),
84+
};
85+
setParams(newParams);
4786
// eslint-disable-next-line react-hooks/exhaustive-deps
48-
}, [initialPage]);
87+
}, []);
4988

5089
const { items = [], pagination = p } = useOpportunities(
5190
page,
5291
params,
5392
initialItems,
5493
p?.count,
55-
true,
94+
!hasFilters && !hasPagination,
5695
apiEndpoint,
5796
);
5897

@@ -81,23 +120,23 @@ const OpportunityList = forwardRef(function OpportunityList(props, ref) {
81120
function handleApplyFilters(filterParams) {
82121
setParams((prev) => ({ ...prev, ...filterParams }));
83122
setPage(1);
84-
const urlParams = new URLSearchParams(router.query);
123+
const searchParams = new URLSearchParams(window.location.search);
85124
Object.entries(filterParams).forEach(([key, value]) => {
86-
if (Array.isArray(value)) {
87-
urlParams.set(key, value.join(","));
125+
if (Array.isArray(value) && value.length > 0) {
126+
searchParams.set(key, value.join(","));
127+
} else if (value) {
128+
searchParams.set(key, value);
88129
} else {
89-
urlParams.set(key, value);
130+
searchParams.delete(key);
90131
}
91132
});
92-
urlParams.delete("page");
93-
router.push(
94-
{
95-
pathname: router.pathname,
96-
query: urlParams.toString(),
97-
},
98-
undefined,
99-
{ shallow: true, scroll: false },
100-
);
133+
searchParams.delete("page");
134+
const queryString = searchParams.toString();
135+
let urlPath = window.location.pathname;
136+
if (queryString) {
137+
urlPath = `${urlPath}?${queryString}`;
138+
}
139+
router.push(urlPath, undefined, { shallow: true, scroll: false });
101140
}
102141

103142
return (

apps/trustlab/src/components/OpportunityList/OpportunityList.test.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@ import OpportunityList from "./OpportunityList";
55

66
import theme from "@/trustlab/theme";
77

8-
// Mock next/router
9-
jest.mock("next/router", () => ({
10-
useRouter: () => ({
11-
push: jest.fn(),
12-
query: {},
13-
pathname: "/opportunities",
14-
}),
15-
}));
16-
178
const render = createRender({ theme });
189

1910
const mockItems = [
@@ -24,8 +15,8 @@ const mockItems = [
2415
root: {
2516
children: [
2617
{
27-
children: [{ text: "Description for item 1" }],
28-
type: "paragraph",
18+
text: "Description for item 1",
19+
type: "text",
2920
},
3021
],
3122
},
@@ -48,8 +39,8 @@ const mockItems = [
4839
root: {
4940
children: [
5041
{
51-
children: [{ text: "Description for item 2" }],
52-
type: "paragraph",
42+
text: "Description for item 2",
43+
type: "text",
5344
},
5445
],
5546
},

apps/trustlab/src/components/OpportunityList/useOpportunities.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function useOpportunities(
88
initialItems,
99
initialCount,
1010
skip,
11-
apiEndpoint = "/api/opportunities",
11+
apiEndpoint = "/api/v1/opportunities",
1212
) {
1313
const searchParams = new URLSearchParams();
1414
searchParams.set("page", page);
@@ -28,6 +28,9 @@ function useOpportunities(
2828
if (params?.search) {
2929
searchParams.set("search", params.search);
3030
}
31+
if (params?.opportunity) {
32+
searchParams.set("opportunity", params.opportunity);
33+
}
3134

3235
const { data } = useSWR(
3336
skip ? null : `${apiEndpoint}?${searchParams.toString()}`,

apps/trustlab/src/components/Testimonial/Testimonial.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const Testimonial = forwardRef(function Testimonial(props, ref) {
4141
TypographyProps={{
4242
variant: "p2",
4343
sx: {
44-
color: "text.secondary",
4544
lineHeight: 1.8,
4645
},
4746
}}
@@ -57,13 +56,13 @@ const Testimonial = forwardRef(function Testimonial(props, ref) {
5756
objectPosition: "left",
5857
maxHeight: 60,
5958
height: "30px",
60-
width: "100%",
59+
width: "95px",
6160
},
6261
}}
6362
sx={{
6463
display: "inline-flex",
6564
height: 60,
66-
width: "100%",
65+
width: "95px",
6766
}}
6867
/>
6968
</Box>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
async function getOpportunityCategoryBlock(block) {
2+
const { categories, settings, ...rest } = block;
3+
4+
const resolvedCategories = await Promise.all(
5+
(categories || []).map(async (category) => {
6+
const image = category.image ?? null;
7+
const link = category.link ?? null;
8+
9+
return {
10+
...category,
11+
image,
12+
link,
13+
};
14+
}),
15+
);
16+
17+
return {
18+
...rest,
19+
slug: "opportunity-category",
20+
categories: resolvedCategories,
21+
settings,
22+
};
23+
}
24+
25+
export default getOpportunityCategoryBlock;

0 commit comments

Comments
 (0)