Skip to content

Commit 457d804

Browse files
Merge pull request #330 from CivicDataLab/collaborative
Add Collaborative feature
2 parents 587a435 + a6d2928 commit 457d804

File tree

27 files changed

+4334
-19
lines changed

27 files changed

+4334
-19
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
'use client';
2+
3+
import BreadCrumbs from '@/components/BreadCrumbs';
4+
import { Icons } from '@/components/icons';
5+
import JsonLd from '@/components/JsonLd';
6+
import { Loading } from '@/components/loading';
7+
import { graphql } from '@/gql';
8+
import { TypeCollaborative } from '@/gql/generated/graphql';
9+
import { GraphQLPublic } from '@/lib/api';
10+
import { formatDate, generateJsonLd } from '@/lib/utils';
11+
import { useQuery } from '@tanstack/react-query';
12+
import Image from 'next/image';
13+
import { Button, Card, Icon, Text } from 'opub-ui';
14+
import { useState } from 'react';
15+
16+
const PublishedCollaboratives = graphql(`
17+
query PublishedCollaboratives {
18+
publishedCollaboratives {
19+
id
20+
title
21+
summary
22+
slug
23+
created
24+
startedOn
25+
completedOn
26+
status
27+
isIndividualCollaborative
28+
user {
29+
fullName
30+
id
31+
profilePicture {
32+
url
33+
}
34+
}
35+
organization {
36+
name
37+
slug
38+
id
39+
logo {
40+
url
41+
}
42+
}
43+
logo {
44+
name
45+
path
46+
}
47+
tags {
48+
id
49+
value
50+
}
51+
sectors {
52+
id
53+
name
54+
}
55+
sdgs {
56+
id
57+
code
58+
name
59+
}
60+
datasetCount
61+
metadata {
62+
metadataItem {
63+
id
64+
label
65+
dataType
66+
}
67+
id
68+
value
69+
}
70+
}
71+
}
72+
`);
73+
74+
const CollaborativesListingClient = () => {
75+
const [searchTerm, setSearchTerm] = useState('');
76+
const [selectedSector, setSelectedSector] = useState('');
77+
78+
const {
79+
data: collaborativesData,
80+
isLoading,
81+
error,
82+
} = useQuery<{ publishedCollaboratives: TypeCollaborative[] }>(
83+
['fetch_published_collaboratives'],
84+
async () => {
85+
console.log('Fetching collaboratives...');
86+
try {
87+
// @ts-expect-error - Query has no variables
88+
const result = await GraphQLPublic(
89+
PublishedCollaboratives as any,
90+
{}
91+
);
92+
console.log('Collaboratives result:', result);
93+
return result as { publishedCollaboratives: TypeCollaborative[] };
94+
} catch (err) {
95+
console.error('Error fetching collaboratives:', err);
96+
throw err;
97+
}
98+
},
99+
{
100+
refetchOnMount: true,
101+
refetchOnReconnect: true,
102+
retry: (failureCount) => {
103+
return failureCount < 3;
104+
},
105+
}
106+
);
107+
108+
const collaboratives = collaborativesData?.publishedCollaboratives || [];
109+
110+
// Filter collaboratives based on search term and sector
111+
const filteredCollaboratives = collaboratives.filter((collaborative) => {
112+
const matchesSearch = collaborative.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
113+
collaborative.summary?.toLowerCase().includes(searchTerm.toLowerCase());
114+
const matchesSector = !selectedSector ||
115+
collaborative.sectors?.some(sector => sector.name === selectedSector);
116+
return matchesSearch && matchesSector;
117+
});
118+
119+
// Get unique sectors for filter dropdown
120+
const allSectors = collaboratives.flatMap((collaborative: TypeCollaborative) =>
121+
collaborative.sectors?.map((sector: any) => sector.name) || []
122+
);
123+
const uniqueSectors = [...new Set(allSectors)];
124+
const jsonLd = generateJsonLd({
125+
'@context': 'https://schema.org',
126+
'@type': 'WebPage',
127+
name: 'CivicDataLab',
128+
url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives`,
129+
description:
130+
'Explore collaborative data initiatives and partnerships that bring organizations together to create impactful solutions.',
131+
});
132+
return (
133+
<main>
134+
<JsonLd json={jsonLd} />
135+
<BreadCrumbs
136+
data={[
137+
{ href: '/', label: 'Home' },
138+
{ href: '/collaboratives', label: 'Collaboratives' },
139+
]}
140+
/>
141+
<>
142+
<>
143+
<div className="w-full">
144+
<div className=" bg-primaryBlue">
145+
<div className=" container flex flex-col-reverse items-center gap-8 py-10 lg:flex-row ">
146+
<div className="flex flex-col gap-5 lg:w-1/2">
147+
<Text
148+
variant="heading2xl"
149+
fontWeight="bold"
150+
color="onBgDefault"
151+
>
152+
Our Collaboratives
153+
</Text>
154+
<Text
155+
variant="headingLg"
156+
color="onBgDefault"
157+
fontWeight="regular"
158+
className="leading-3 lg:leading-5"
159+
>
160+
By Collaboratives we mean a collective effort by several organisations
161+
in any specific sectors that can be applied to address some of the
162+
most pressing concerns from hyper-local to the global level simultaneously.
163+
</Text>
164+
</div>
165+
<div className="flex w-full items-center justify-center lg:w-1/2">
166+
<Image
167+
src={'/collaborative.png'}
168+
alt={'collaborative'}
169+
width={1700}
170+
height={800}
171+
className="h-auto w-full object-contain"
172+
/>
173+
</div>
174+
</div>
175+
</div>
176+
</div>
177+
</>
178+
</>
179+
180+
<div className="bg-onSurfaceDefault">
181+
<div className="container py-8 lg:py-14">
182+
{/* Header Section */}
183+
<div className="mb-8">
184+
185+
{/* Search and Filter Section */}
186+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:gap-6">
187+
<div className="flex-1">
188+
<input
189+
type="text"
190+
placeholder="Search collaboratives..."
191+
value={searchTerm}
192+
onChange={(e) => setSearchTerm(e.target.value)}
193+
className="w-full rounded-lg border border-greyExtralight px-4 py-2 focus:border-primaryBlue focus:outline-none"
194+
/>
195+
</div>
196+
<div className="md:w-48">
197+
<select
198+
value={selectedSector}
199+
onChange={(e) => setSelectedSector(e.target.value)}
200+
className="w-full rounded-lg border border-greyExtralight px-4 py-2 focus:border-primaryBlue focus:outline-none"
201+
>
202+
<option value="">All Sectors</option>
203+
{uniqueSectors.map((sector: string) => (
204+
<option key={sector} value={sector}>
205+
{sector}
206+
</option>
207+
))}
208+
</select>
209+
</div>
210+
</div>
211+
</div>
212+
213+
{isLoading? (
214+
<div className="flex justify-center p-10">
215+
<Loading />
216+
</div>
217+
):error?(
218+
<div className="flex flex-col items-center justify-center gap-4 py-10">
219+
<Text variant="headingXl" color="critical">
220+
Error Loading Collaboratives
221+
</Text>
222+
<Text variant="bodyLg">
223+
Failed to load collaboratives. Please try again later.
224+
</Text>
225+
</div>
226+
):null}
227+
228+
{/* Results Section */}
229+
{!isLoading && !error && (
230+
<>
231+
<div className="mb-6 flex items-center justify-between">
232+
<Text variant="headingLg">
233+
{filteredCollaboratives.length} Collaborative{filteredCollaboratives.length !== 1 ? 's' : ''} Found
234+
</Text>
235+
</div>
236+
237+
{/* Collaboratives Grid */}
238+
{filteredCollaboratives.length > 0 ? (
239+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
240+
{filteredCollaboratives.map((collaborative: TypeCollaborative) => (
241+
<Card
242+
key={collaborative.id}
243+
title={collaborative.title || ''}
244+
variation="collapsed"
245+
iconColor="warning"
246+
metadataContent={[
247+
{
248+
icon: Icons.calendar,
249+
label: 'Started',
250+
value: formatDate(collaborative.startedOn),
251+
},
252+
{
253+
icon: Icons.dataset,
254+
label: 'Datasets',
255+
value: collaborative.datasetCount?.toString() || '0',
256+
},
257+
{
258+
icon: Icons.globe,
259+
label: 'Geography',
260+
value:
261+
collaborative.metadata?.find(
262+
(meta: any) =>
263+
meta.metadataItem?.label === 'Geography'
264+
)?.value || 'N/A',
265+
},
266+
]}
267+
href={`/collaboratives/${collaborative.slug}`}
268+
footerContent={[
269+
{
270+
icon: collaborative.sectors?.[0]?.name
271+
? `/Sectors/${collaborative.sectors[0].name}.svg`
272+
: '/Sectors/default.svg',
273+
label: 'Sectors',
274+
},
275+
{
276+
icon: collaborative.isIndividualCollaborative
277+
? collaborative?.user?.profilePicture
278+
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${collaborative.user.profilePicture.url}`
279+
: '/profile.png'
280+
: collaborative?.organization?.logo
281+
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${collaborative.organization.logo.url}`
282+
: '/org.png',
283+
label: 'Published by',
284+
},
285+
]}
286+
description={collaborative.summary || ''}
287+
/>
288+
))}
289+
</div>
290+
) : (
291+
<div className="flex flex-col items-center justify-center gap-4 py-20">
292+
<Icon source={Icons.search} size={48} color="subdued" />
293+
<Text variant="headingLg" color="subdued">
294+
No Collaboratives Found
295+
</Text>
296+
<Text variant="bodyLg" color="subdued">
297+
Try adjusting your search terms or filters.
298+
</Text>
299+
{(searchTerm || selectedSector) && (
300+
<Button
301+
onClick={() => {
302+
setSearchTerm('');
303+
setSelectedSector('');
304+
}}
305+
kind="secondary"
306+
>
307+
Clear Filters
308+
</Button>
309+
)}
310+
</div>
311+
)}
312+
</>
313+
)}
314+
</div>
315+
</div>
316+
</main>
317+
);
318+
};
319+
320+
export default CollaborativesListingClient;

0 commit comments

Comments
 (0)