diff --git a/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx b/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx
new file mode 100644
index 00000000..8b5073b9
--- /dev/null
+++ b/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx
@@ -0,0 +1,320 @@
+'use client';
+
+import BreadCrumbs from '@/components/BreadCrumbs';
+import { Icons } from '@/components/icons';
+import JsonLd from '@/components/JsonLd';
+import { Loading } from '@/components/loading';
+import { graphql } from '@/gql';
+import { TypeCollaborative } from '@/gql/generated/graphql';
+import { GraphQLPublic } from '@/lib/api';
+import { formatDate, generateJsonLd } from '@/lib/utils';
+import { useQuery } from '@tanstack/react-query';
+import Image from 'next/image';
+import { Button, Card, Icon, Text } from 'opub-ui';
+import { useState } from 'react';
+
+const PublishedCollaboratives = graphql(`
+ query PublishedCollaboratives {
+ publishedCollaboratives {
+ id
+ title
+ summary
+ slug
+ created
+ startedOn
+ completedOn
+ status
+ isIndividualCollaborative
+ user {
+ fullName
+ id
+ profilePicture {
+ url
+ }
+ }
+ organization {
+ name
+ slug
+ id
+ logo {
+ url
+ }
+ }
+ logo {
+ name
+ path
+ }
+ tags {
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ sdgs {
+ id
+ code
+ name
+ }
+ datasetCount
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ }
+ }
+`);
+
+const CollaborativesListingClient = () => {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedSector, setSelectedSector] = useState('');
+
+ const {
+ data: collaborativesData,
+ isLoading,
+ error,
+ } = useQuery<{ publishedCollaboratives: TypeCollaborative[] }>(
+ ['fetch_published_collaboratives'],
+ async () => {
+ console.log('Fetching collaboratives...');
+ try {
+ // @ts-expect-error - Query has no variables
+ const result = await GraphQLPublic(
+ PublishedCollaboratives as any,
+ {}
+ );
+ console.log('Collaboratives result:', result);
+ return result as { publishedCollaboratives: TypeCollaborative[] };
+ } catch (err) {
+ console.error('Error fetching collaboratives:', err);
+ throw err;
+ }
+ },
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ retry: (failureCount) => {
+ return failureCount < 3;
+ },
+ }
+ );
+
+ const collaboratives = collaborativesData?.publishedCollaboratives || [];
+
+ // Filter collaboratives based on search term and sector
+ const filteredCollaboratives = collaboratives.filter((collaborative) => {
+ const matchesSearch = collaborative.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ collaborative.summary?.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesSector = !selectedSector ||
+ collaborative.sectors?.some(sector => sector.name === selectedSector);
+ return matchesSearch && matchesSector;
+ });
+
+ // Get unique sectors for filter dropdown
+ const allSectors = collaboratives.flatMap((collaborative: TypeCollaborative) =>
+ collaborative.sectors?.map((sector: any) => sector.name) || []
+ );
+ const uniqueSectors = [...new Set(allSectors)];
+ const jsonLd = generateJsonLd({
+ '@context': 'https://schema.org',
+ '@type': 'WebPage',
+ name: 'CivicDataLab',
+ url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives`,
+ description:
+ 'Explore collaborative data initiatives and partnerships that bring organizations together to create impactful solutions.',
+ });
+ return (
+
+
+
+ <>
+ <>
+
+
+
+
+
+ Our Collaboratives
+
+
+ By Collaboratives we mean a collective effort by several organisations
+ in any specific sectors that can be applied to address some of the
+ most pressing concerns from hyper-local to the global level simultaneously.
+
+
+
+
+
+
+
+
+ >
+ >
+
+
+
+ {/* Header Section */}
+
+
+ {/* Search and Filter Section */}
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full rounded-lg border border-greyExtralight px-4 py-2 focus:border-primaryBlue focus:outline-none"
+ />
+
+
+ setSelectedSector(e.target.value)}
+ className="w-full rounded-lg border border-greyExtralight px-4 py-2 focus:border-primaryBlue focus:outline-none"
+ >
+ All Sectors
+ {uniqueSectors.map((sector: string) => (
+
+ {sector}
+
+ ))}
+
+
+
+
+
+ {isLoading? (
+
+
+
+ ):error?(
+
+
+ Error Loading Collaboratives
+
+
+ Failed to load collaboratives. Please try again later.
+
+
+ ):null}
+
+ {/* Results Section */}
+ {!isLoading && !error && (
+ <>
+
+
+ {filteredCollaboratives.length} Collaborative{filteredCollaboratives.length !== 1 ? 's' : ''} Found
+
+
+
+ {/* Collaboratives Grid */}
+ {filteredCollaboratives.length > 0 ? (
+
+ {filteredCollaboratives.map((collaborative: TypeCollaborative) => (
+
+ meta.metadataItem?.label === 'Geography'
+ )?.value || 'N/A',
+ },
+ ]}
+ href={`/collaboratives/${collaborative.slug}`}
+ footerContent={[
+ {
+ icon: collaborative.sectors?.[0]?.name
+ ? `/Sectors/${collaborative.sectors[0].name}.svg`
+ : '/Sectors/default.svg',
+ label: 'Sectors',
+ },
+ {
+ icon: collaborative.isIndividualCollaborative
+ ? collaborative?.user?.profilePicture
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${collaborative.user.profilePicture.url}`
+ : '/profile.png'
+ : collaborative?.organization?.logo
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${collaborative.organization.logo.url}`
+ : '/org.png',
+ label: 'Published by',
+ },
+ ]}
+ description={collaborative.summary || ''}
+ />
+ ))}
+
+ ) : (
+
+
+
+ No Collaboratives Found
+
+
+ Try adjusting your search terms or filters.
+
+ {(searchTerm || selectedSector) && (
+ {
+ setSearchTerm('');
+ setSelectedSector('');
+ }}
+ kind="secondary"
+ >
+ Clear Filters
+
+ )}
+
+ )}
+ >
+ )}
+
+
+
+ );
+};
+
+export default CollaborativesListingClient;
diff --git a/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx
new file mode 100644
index 00000000..a3725eb2
--- /dev/null
+++ b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx
@@ -0,0 +1,532 @@
+'use client';
+
+import { graphql } from '@/gql';
+import { TypeCollaborative, TypeDataset, TypeUseCase } from '@/gql/generated/graphql';
+import { useQuery } from '@tanstack/react-query';
+import Image from 'next/image';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+import { Card, Text } from 'opub-ui';
+import { useEffect } from 'react';
+
+import BreadCrumbs from '@/components/BreadCrumbs';
+import { Icons } from '@/components/icons';
+import JsonLd from '@/components/JsonLd';
+import { Loading } from '@/components/loading';
+import { useAnalytics } from '@/hooks/use-analytics';
+import { GraphQLPublic } from '@/lib/api';
+import { formatDate, generateJsonLd } from '@/lib/utils';
+import PrimaryDetails from '../components/Details';
+import Metadata from '../components/Metadata';
+
+const CollaborativeDetails = graphql(`
+ query CollaborativeQuery($slug: String!) {
+ collaborativeBySlug(slug: $slug) {
+ id
+ title
+ summary
+ created
+ startedOn
+ completedOn
+ isIndividualCollaborative
+ user {
+ fullName
+ email
+ id
+ profilePicture {
+ url
+ }
+ }
+ organization {
+ name
+ slug
+ id
+ contactEmail
+ logo {
+ url
+ }
+ }
+ platformUrl
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ sdgs {
+ id
+ code
+ name
+ }
+ tags {
+ id
+ value
+ }
+ geographies {
+ id
+ name
+ code
+ type
+ }
+ publishers {
+ name
+ contactEmail
+ logo {
+ url
+ }
+ }
+ logo {
+ name
+ path
+ }
+ coverImage {
+ name
+ path
+ }
+ datasets {
+ title
+ id
+ isIndividualDataset
+ user {
+ fullName
+ id
+ profilePicture {
+ url
+ }
+ }
+ downloadCount
+ description
+ organization {
+ name
+ logo {
+ url
+ }
+ }
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ sectors {
+ name
+ }
+ modified
+ }
+ useCases {
+ id
+ title
+ summary
+ slug
+ startedOn
+ completedOn
+ runningStatus
+ isIndividualUsecase
+ user {
+ fullName
+ id
+ profilePicture {
+ url
+ }
+ }
+ organization {
+ name
+ slug
+ id
+ logo {
+ url
+ }
+ }
+ logo {
+ name
+ path
+ }
+ sectors {
+ name
+ }
+ tags {
+ id
+ value
+ }
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ modified
+ }
+ contactEmail
+ status
+ slug
+ modified
+ contributors {
+ id
+ fullName
+ profilePicture {
+ url
+ }
+ }
+ supportingOrganizations {
+ id
+ slug
+ name
+ logo {
+ url
+ }
+ }
+ partnerOrganizations {
+ id
+ slug
+ name
+ logo {
+ url
+ }
+ }
+ }
+ }
+`);
+
+const CollaborativeDetailClient = () => {
+ const params = useParams();
+ const { trackCollaborative } = useAnalytics();
+
+ const {
+ data: CollaborativeDetailsData,
+ isLoading,
+ error,
+ } = useQuery<{ collaborativeBySlug: TypeCollaborative }>(
+ [`fetch_CollaborativeDetails_${params.collaborativeSlug}`],
+ async () => {
+ console.log('Fetching collaborative details for:', params.collaborativeSlug);
+ const result = await GraphQLPublic(
+ CollaborativeDetails as any,
+ {},
+ {
+ slug: params.collaborativeSlug,
+ }
+ ) as { collaborativeBySlug: TypeCollaborative };
+ return result;
+ },
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ retry: (failureCount) => {
+ return failureCount < 3;
+ },
+ }
+ );
+
+ console.log('Collaborative details query state:', { isLoading, error, data: CollaborativeDetailsData });
+
+ // Track collaborative view when data is loaded
+ useEffect(() => {
+ if (CollaborativeDetailsData?.collaborativeBySlug) {
+ trackCollaborative(CollaborativeDetailsData.collaborativeBySlug.id, CollaborativeDetailsData.collaborativeBySlug.title || undefined);
+ }
+ }, [CollaborativeDetailsData?.collaborativeBySlug, trackCollaborative]);
+
+ const datasets = CollaborativeDetailsData?.collaborativeBySlug?.datasets || []; // Fallback to an empty array
+ const useCases = CollaborativeDetailsData?.collaborativeBySlug?.useCases || []; // Fallback to an empty array
+
+ const hasSupportingOrganizations =
+ CollaborativeDetailsData?.collaborativeBySlug?.supportingOrganizations &&
+ CollaborativeDetailsData?.collaborativeBySlug?.supportingOrganizations?.length > 0;
+ const hasPartnerOrganizations =
+ CollaborativeDetailsData?.collaborativeBySlug?.partnerOrganizations &&
+ CollaborativeDetailsData?.collaborativeBySlug?.partnerOrganizations?.length > 0;
+ const hasContributors =
+ CollaborativeDetailsData?.collaborativeBySlug?.contributors &&
+ CollaborativeDetailsData?.collaborativeBySlug?.contributors?.length > 0;
+
+ const jsonLd = generateJsonLd({
+ '@context': 'https://schema.org',
+ '@type': 'WebPage',
+ name: 'CivicDataLab',
+ url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives/${params.collaborativeSlug}`,
+ description:
+ CollaborativeDetailsData?.collaborativeBySlug?.summary ||
+ `Explore open data and curated datasets in the ${CollaborativeDetailsData?.collaborativeBySlug?.title} collaborative.`,
+ publisher: {
+ '@type': 'Organization',
+ name: 'CivicDataSpace',
+ url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives/${params.collaborativeSlug}`,
+ },
+ });
+
+ return (
+ <>
+
+
+ {isLoading ? (
+
+
+
+ ) : error ? (
+
+
+
+ Error Loading Collaborative
+
+
+ {(error as any)?.message?.includes('401') || (error as any)?.message?.includes('403')
+ ? 'You do not have permission to view this collaborative. Please log in or contact the administrator.'
+ : 'Failed to load collaborative details. Please try again later.'}
+
+
+
+ ) : !CollaborativeDetailsData?.collaborativeBySlug ? (
+
+
+ Collaborative Not Found
+
+ The requested collaborative could not be found.
+
+
+
+ ) : (
+ <>
+
+
+
+ {(hasSupportingOrganizations || hasPartnerOrganizations) && (
+
+
+ {hasSupportingOrganizations && (
+
+
+ Supported by
+
+
+ {CollaborativeDetailsData?.collaborativeBySlug?.supportingOrganizations?.map(
+ (org: any) => (
+
+
+
+
+
+ )
+ )}
+
+
+ )}
+ {hasPartnerOrganizations && (
+
+
+ Partnered by
+
+
+ {CollaborativeDetailsData?.collaborativeBySlug?.partnerOrganizations?.map(
+ (org: any) => (
+
+
+
+
+
+ )
+ )}
+
+
+ )}
+
+
+ )}
+
+
+ {/* Use Cases Section */}
+ {useCases.length > 0 && (
+
+
+ Use Cases
+
+ Use Cases associated with this Collaborative
+
+
+
+ {useCases.map((useCase: TypeUseCase) => {
+ const image = useCase.isIndividualUsecase
+ ? useCase?.user?.profilePicture
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${useCase.user.profilePicture.url}`
+ : '/profile.png'
+ : useCase?.organization?.logo
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${useCase.organization.logo.url}`
+ : '/org.png';
+
+ const Geography = useCase.metadata?.find(
+ (meta: any) => meta.metadataItem?.label === 'Geography'
+ )?.value;
+
+ const MetadataContent = [
+ {
+ icon: Icons.calendar,
+ label: 'Date',
+ value: formatDate(useCase.modified),
+ tooltip: 'Date',
+ },
+ ];
+
+ if (Geography) {
+ MetadataContent.push({
+ icon: Icons.globe,
+ label: 'Geography',
+ value: Geography,
+ tooltip: 'Geography',
+ });
+ }
+
+ const FooterContent = [
+ {
+ icon: useCase.sectors && useCase.sectors[0]?.name
+ ? `/Sectors/${useCase.sectors[0].name}.svg`
+ : '/Sectors/default.svg',
+ label: 'Sectors',
+ tooltip: useCase.sectors?.[0]?.name || 'Sector',
+ },
+ {
+ icon: image,
+ label: 'Published by',
+ tooltip: useCase.isIndividualUsecase
+ ? useCase.user?.fullName
+ : useCase.organization?.name,
+ },
+ ];
+
+ const commonProps = {
+ title: useCase.title || '',
+ description: useCase.summary || '',
+ metadataContent: MetadataContent,
+ tag: useCase.tags?.map((t: any) => t.value) || [],
+ footerContent: FooterContent,
+ imageUrl: '',
+ };
+
+ if (useCase.logo) {
+ commonProps.imageUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}/${useCase.logo.path.replace('/code/files/', '')}`;
+ }
+
+ return (
+
+ );
+ })}
+
+
+ )}
+ {/* Datasets Section */}
+
+
+ Datasets in this Collaborative
+
+ Explore datasets related to this collaborative{' '}
+
+
+
+ {datasets.length > 0 &&
+ datasets.map((dataset: TypeDataset) => (
+
+ meta.metadataItem?.label === 'Geography'
+ )?.value || '',
+ },
+ ]}
+ href={`/datasets/${dataset.id}`}
+ footerContent={[
+ {
+ icon: `/Sectors/${dataset.sectors[0]?.name}.svg`,
+ label: 'Sectors',
+ },
+ {
+ icon: dataset.isIndividualDataset
+ ? dataset?.user?.profilePicture
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${dataset.user.profilePicture.url}`
+ : '/profile.png'
+ : dataset?.organization?.logo
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${dataset.organization.logo.url}`
+ : '/org.png',
+ label: 'Published by',
+ },
+ ]}
+ description={dataset.description || ''}
+ />
+ ))}
+
+
+
+
+ >
+ )}
+
+ >
+ );
+};
+
+export default CollaborativeDetailClient;
diff --git a/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx
new file mode 100644
index 00000000..4be3188a
--- /dev/null
+++ b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx
@@ -0,0 +1,74 @@
+import { Metadata } from 'next';
+import { graphql } from '@/gql';
+
+import { GraphQLPublic } from '@/lib/api';
+import { generatePageMetadata } from '@/lib/utils';
+import CollaborativeDetailClient from './CollaborativeDetailsClient';
+
+const CollaborativeInfoQuery = graphql(`
+ query CollaborativeInfo($pk: ID!) {
+ collaborative(pk: $pk) {
+ id
+ title
+ summary
+ slug
+ logo {
+ path
+ }
+ tags {
+ id
+ value
+ }
+ }
+ }
+`);
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { collaborativeSlug: string };
+}): Promise {
+ try {
+ const data = await GraphQLPublic(CollaborativeInfoQuery, {}, { pk: params.collaborativeSlug });
+ const Collaborative = data?.collaborative;
+
+ return generatePageMetadata({
+ title: `${Collaborative?.title} | Collaborative Data | CivicDataSpace`,
+ description:
+ Collaborative?.summary ||
+ `Explore open data and curated datasets in the ${Collaborative?.title} collaborative.`,
+ keywords: Collaborative?.tags?.map((tag: any) => tag.value) || [],
+ openGraph: {
+ type: 'article',
+ locale: 'en_US',
+ url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives/${params.collaborativeSlug}`,
+ title: `${Collaborative?.title} | Collaborative Data | CivicDataSpace`,
+ description:
+ Collaborative?.summary ||
+ `Explore open data and curated datasets in the ${Collaborative?.title} collaborative.`,
+ siteName: 'CivicDataSpace',
+ image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`,
+ },
+ });
+ } catch (error) {
+ // Fallback to generic metadata if the API call fails
+ return generatePageMetadata({
+ title: `Collaborative Details | CivicDataSpace`,
+ description: `Explore open data and curated datasets in this collaborative.`,
+ keywords: ['collaborative', 'data', 'civic', 'open data'],
+ openGraph: {
+ type: 'article',
+ locale: 'en_US',
+ url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives/${params.collaborativeSlug}`,
+ title: `Collaborative Details | CivicDataSpace`,
+ description: `Explore open data and curated datasets in this collaborative.`,
+ siteName: 'CivicDataSpace',
+ image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`,
+ },
+ });
+ }
+}
+
+export default function Page() {
+ return ;
+}
diff --git a/app/[locale]/(user)/collaboratives/components/Details.tsx b/app/[locale]/(user)/collaboratives/components/Details.tsx
new file mode 100644
index 00000000..4791294d
--- /dev/null
+++ b/app/[locale]/(user)/collaboratives/components/Details.tsx
@@ -0,0 +1,147 @@
+'use client';
+
+import React, { useState } from 'react';
+import Image from 'next/image';
+import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui';
+
+import { Icons } from '@/components/icons';
+import Metadata from './Metadata';
+
+const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+ {data.collaborativeBySlug.title}
+
+
+ {data.collaborativeBySlug.tags.map((item: any, index: number) => (
+
+
+ {item.value}
+
+
+ ))}
+
+
+
+ setOpen(true)}
+ >
+
+
+ Metadata
+
+
+
+ }
+ >
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {data.collaborativeBySlug.coverImage && (
+
+
+
+ )}
+
+ {/* Stats Section */}
+
+
+
+ {data.collaborativeBySlug.useCases?.length || 0}
+
+
+ Use Cases
+
+
+
+
+
+ {data.collaborativeBySlug.datasets?.length || 0}
+
+
+ Datasets
+
+
+
+
+
+ {(data.collaborativeBySlug.supportingOrganizations?.length || 0) +
+ (data.collaborativeBySlug.partnerOrganizations?.length || 0)}
+
+
+ Organizations
+
+
+
+
+
+ {data.collaborativeBySlug.contributors?.length || 0}
+
+
+ Contributors
+
+
+
+
+
+ {data.collaborativeBySlug.geographies && data.collaborativeBySlug.geographies.length > 0 && (
+
+
Geographies
+
+ {data.collaborativeBySlug.geographies.map((geo: any, index: number) => (
+
+ {geo.name}
+
+ ))}
+
+
+ )}
+
+
Summary
+
+
+ {data.collaborativeBySlug.summary}
+
+
+
+
+
+ );
+};
+
+export default PrimaryDetails;
diff --git a/app/[locale]/(user)/collaboratives/components/Metadata.tsx b/app/[locale]/(user)/collaboratives/components/Metadata.tsx
new file mode 100644
index 00000000..cc8f54cb
--- /dev/null
+++ b/app/[locale]/(user)/collaboratives/components/Metadata.tsx
@@ -0,0 +1,201 @@
+import Image from 'next/image';
+import Link from 'next/link';
+import { Button, Divider, Icon, Text, Tooltip } from 'opub-ui';
+import { useEffect, useState } from 'react';
+
+import { Icons } from '@/components/icons';
+import { formatDate, getWebsiteTitle } from '@/lib/utils';
+
+const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => {
+ const [platformTitle, setPlatformTitle] = useState(null);
+
+ useEffect(() => {
+ const fetchTitle = async () => {
+ try {
+ const urlItem = data.collaborativeBySlug.platformUrl;
+
+ if (urlItem && urlItem.value) {
+ const title = await getWebsiteTitle(urlItem.value);
+ setPlatformTitle(title);
+ }
+ } catch (error) {
+ console.error('Error fetching website title:', error);
+ }
+ };
+
+ if (data.collaborativeBySlug.platformUrl === null) {
+ setPlatformTitle('N/A');
+ } else {
+ fetchTitle();
+ }
+ }, [data.collaborativeBySlug.platformUrl]);
+
+ const metadata = [
+ {
+ label: 'Platform URL',
+ value:
+ data.collaborativeBySlug.platformUrl === null ? (
+ 'N/A'
+ ) : (
+
+
+ {platformTitle?.trim() ? platformTitle : 'Visit Platform'}
+
+
+ ),
+ tooltipContent: data.collaborativeBySlug.platformUrl === null ? 'N/A' : platformTitle,
+ },
+ {
+ label: 'Last Updated',
+ value: formatDate(data.collaborativeBySlug.modified) || 'N/A',
+ tooltipContent: formatDate(data.collaborativeBySlug.modified) || 'N/A',
+ },
+ {
+ label: 'Sectors',
+ value: (
+
+ {data.collaborativeBySlug.sectors.length > 0 ? (
+ data.collaborativeBySlug.sectors.map((sector: any, index: number) => (
+
+
+
+ ))
+ ) : (
+ N/A // Fallback if no sectors are available
+ )}
+
+ ),
+ },
+ {
+ label: 'SDG Goals',
+ value: (
+
+ {data.collaborativeBySlug.sdgs && data.collaborativeBySlug.sdgs.length > 0 ? (
+ data.collaborativeBySlug.sdgs.map((sdg: any, index: number) => (
+
+
+
+ ))
+ ) : (
+ N/A
+ )}
+
+ ),
+ },
+ ];
+
+ // Use collaborative logo if available, otherwise use a default
+ const image = data.collaborativeBySlug?.logo?.path
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${data.collaborativeBySlug.logo.path.replace('/code/files/', '')}`
+ : '/org.png';
+
+ return (
+
+
+
+
+ ABOUT THE COLLABORATIVE{' '}
+
+ DETAILS
+
+
+ {setOpen && (
+ setOpen(false)} kind="tertiary">
+
+
+ )}
+
+
+
+
+
+
+
+
+ {metadata.map((item, index) => (
+
+
+ {item.label}
+
+
+
+ {typeof item.value === 'string' ? item.value : item.value}
+
+
+
+ ))}
+ {/* Contributors Section */}
+ {data.collaborativeBySlug.contributors && data.collaborativeBySlug.contributors.length > 0 && (
+
+
+ Contributors
+
+
+ {data.collaborativeBySlug.contributors.map((contributor: any) => (
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+export default Metadata;
diff --git a/app/[locale]/(user)/collaboratives/page.tsx b/app/[locale]/(user)/collaboratives/page.tsx
new file mode 100644
index 00000000..9db3a7e4
--- /dev/null
+++ b/app/[locale]/(user)/collaboratives/page.tsx
@@ -0,0 +1,25 @@
+import { Metadata } from 'next';
+
+import { generatePageMetadata } from '@/lib/utils';
+import CollaborativesListingClient from './CollaborativesListingClient';
+
+export const metadata: Metadata = generatePageMetadata({
+ title: 'Collaboratives | CivicDataSpace',
+ description:
+ 'Explore collaborative data initiatives and partnerships. Discover how organizations work together to create impactful data solutions.',
+ keywords: ['collaboratives', 'partnerships', 'data collaboration', 'civic data'],
+ openGraph: {
+ type: 'website',
+ locale: 'en_US',
+ url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/collaboratives`,
+ title: 'Collaboratives | CivicDataSpace',
+ description:
+ 'Explore collaborative data initiatives and partnerships. Discover how organizations work together to create impactful data solutions.',
+ siteName: 'CivicDataSpace',
+ image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`,
+ },
+});
+
+export default function Page() {
+ return ;
+}
diff --git a/app/[locale]/(user)/components/ListingComponent.tsx b/app/[locale]/(user)/components/ListingComponent.tsx
index 5946684d..851be87b 100644
--- a/app/[locale]/(user)/components/ListingComponent.tsx
+++ b/app/[locale]/(user)/components/ListingComponent.tsx
@@ -281,10 +281,13 @@ const ListingComponent: React.FC = ({
const filterOptions = Object.entries(aggregations).reduce(
(acc: Record, [key, value]) => {
- acc[key] = Object.entries(value).map(([bucketKey]) => ({
- label: bucketKey,
- value: bucketKey,
- }));
+ // Check if value exists and has buckets array
+ if (value && value.buckets && Array.isArray(value.buckets)) {
+ acc[key] = value.buckets.map((bucket) => ({
+ label: bucket.key,
+ value: bucket.key,
+ }));
+ }
return acc;
},
{}
diff --git a/app/[locale]/(user)/usecases/components/Metadata.tsx b/app/[locale]/(user)/usecases/components/Metadata.tsx
index 2f427b52..85ab8dc3 100644
--- a/app/[locale]/(user)/usecases/components/Metadata.tsx
+++ b/app/[locale]/(user)/usecases/components/Metadata.tsx
@@ -133,21 +133,18 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => {
label: 'SDG Goals',
value: (
- {data.useCase.metadata.length > 0 ? (
- data.useCase.metadata
- ?.find((meta: any) => meta.metadataItem?.label === 'SDG Goal')
- ?.value.split(', ')
- .map((item: any, index: number) => (
-
-
-
- ))
+ {data.useCase.sdgs && data.useCase.sdgs.length > 0 ? (
+ data.useCase.sdgs.map((sdg: any, index: number) => (
+
+
+
+ ))
) : (
N/A
)}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx
new file mode 100644
index 00000000..3b11195d
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx
@@ -0,0 +1,178 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+import { useParams, useRouter } from 'next/navigation';
+import { fetchDatasets } from '@/fetch';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Button, DataTable, Text, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { formatDate } from '@/lib/utils';
+import { Loading } from '@/components/loading';
+
+const FetchCollaborativeDetails: any = graphql(`
+ query CollaborativeDetails($filters: CollaborativeFilter) {
+ collaboratives(filters: $filters) {
+ id
+ title
+ datasets {
+ id
+ title
+ modified
+ sectors {
+ name
+ }
+ }
+ }
+ }
+`);
+
+const AssignCollaborativeDatasets: any = graphql(`
+ mutation assignCollaborativeDatasets($collaborativeId: String!, $datasetIds: [UUID!]!) {
+ updateCollaborativeDatasets(collaborativeId: $collaborativeId, datasetIds: $datasetIds) {
+ ... on TypeCollaborative {
+ id
+ datasets {
+ id
+ title
+ }
+ }
+ }
+ }
+`);
+const Assign = () => {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+ const router = useRouter();
+
+ const [data, setData] = useState
([]); // Ensure `data` is an array
+ const [selectedRow, setSelectedRows] = useState([]);
+
+ const CollaborativeDetails: { data: any; isLoading: boolean; refetch: any } =
+ useQuery(
+ [`Collaborative_Details`, params.id],
+ () =>
+ GraphQL(
+ FetchCollaborativeDetails,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: params.id,
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const formattedData = (data: any) =>
+ data.map((item: any) => {
+ return {
+ title: item.title,
+ id: item.id,
+ category: item.sectors[0]?.name || 'N/A', // Safeguard in case of missing category
+ modified: formatDate(item.modified),
+ };
+ });
+
+ useEffect(() => {
+ fetchDatasets('?size=1000&page=1')
+ .then((res) => {
+ setData(res.results);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }, []);
+
+ const columns = [
+ { accessorKey: 'title', header: 'Title' },
+ { accessorKey: 'category', header: 'Sector' },
+ { accessorKey: 'modified', header: 'Last Modified' },
+ ];
+
+ const generateTableData = (list: Array) => {
+ return list.map((item) => {
+ return {
+ title: item.title,
+ id: item.id,
+ category: item.sectors[0],
+ modified: formatDate(item.modified),
+ };
+ });
+ };
+
+ const { mutate, isLoading: mutationLoading } = useMutation(
+ () =>
+ GraphQL(
+ AssignCollaborativeDatasets,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ collaborativeId: params.id,
+ datasetIds: Array.isArray(selectedRow)
+ ? selectedRow.map((row: any) => row.id)
+ : [],
+ }
+ ),
+ {
+ onSuccess: (data: any) => {
+ toast('Dataset Assigned Successfully');
+ CollaborativeDetails.refetch();
+ router.push(
+ `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/contributors`
+ );
+ },
+ onError: (err: any) => {
+ toast(`Received ${err} on dataset publish `);
+ },
+ }
+ );
+
+ return (
+ <>
+ {CollaborativeDetails?.data?.collaboratives[0]?.datasets?.length >= 0 &&
+ data.length > 0 &&
+ !CollaborativeDetails.isLoading ? (
+ <>
+
+
+
+ Selected {selectedRow.length} of {data.length}
+
+
+
+ mutate()}>
+ Submit
+
+
+
+
+ {
+ setSelectedRows(Array.isArray(selected) ? selected : []); // Ensure selected is always an array
+ }}
+ />
+ >
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default Assign;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx
new file mode 100644
index 00000000..d58be3dd
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx
@@ -0,0 +1,88 @@
+import Image from 'next/image';
+import { Button, Icon, Text } from 'opub-ui';
+
+import { Icons } from '@/components/icons';
+import CustomCombobox from '../../../../usecases/edit/[id]/contributors/CustomCombobox';
+
+type Option = { label: string; value: string };
+
+type EntitySectionProps = {
+ title: string;
+ label: string;
+ placeholder: string;
+ options: Option[];
+ selectedValues: Option[];
+ onChange: (values: Option[]) => void;
+ onRemove: (value: Option) => void;
+ data: any;
+};
+
+const EntitySection = ({
+ title,
+ label,
+ placeholder,
+ options,
+ selectedValues,
+ onChange,
+ onRemove,
+ data,
+}: EntitySectionProps) => (
+
+
{title}
+
+
+
+ {label}
+
+ onChange([
+ ...selectedValues,
+ ...value.filter(
+ (val) =>
+ !selectedValues.some((item) => item.value === val.value)
+ ),
+ ])
+ }
+ />
+
+
+ {selectedValues.map((item) => (
+
+
+ org.id === item.value)?.logo?.url
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${
+ data?.find((org: any) => org.id === item.value)?.logo
+ ?.url
+ }`
+ : '/org.png'
+ }
+ alt={item.label}
+ width={140}
+ height={100}
+ className="object-contain"
+ />
+
+
onRemove(item)}>
+
+
+
+ {item.label}
+
+
+
+
+
+
+ ))}
+
+
+
+
+);
+
+export default EntitySection;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx
new file mode 100644
index 00000000..d49baa4f
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx
@@ -0,0 +1,412 @@
+'use client';
+
+import { useMutation, useQuery } from '@tanstack/react-query';
+import Image from 'next/image';
+import { useParams } from 'next/navigation';
+import { Button, Icon, Text, toast } from 'opub-ui';
+import { useEffect, useState } from 'react';
+
+import { Icons } from '@/components/icons';
+import { Loading } from '@/components/loading';
+import { GraphQL } from '@/lib/api';
+import { useEditStatus } from '../../context';
+import CustomCombobox from '../../../../usecases/edit/[id]/contributors/CustomCombobox';
+import EntitySection from './EntitySelection';
+import {
+ AddContributors,
+ AddPartners,
+ AddSupporters,
+ FetchCollaborativeInfo,
+ FetchUsers,
+ OrgList,
+ RemoveContributor,
+ RemovePartners,
+ RemoveSupporters,
+} from './query';
+
+const Details = () => {
+ const params = useParams<{ entityType: string; entitySlug: string; id: string }>();
+ const [searchValue, setSearchValue] = useState('');
+ const [formData, setFormData] = useState({
+ contributors: [] as { label: string; value: string }[],
+ supporters: [] as { label: string; value: string }[],
+ partners: [] as { label: string; value: string }[],
+ });
+
+ const Users: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_users`],
+ () =>
+ GraphQL(
+ FetchUsers,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ limit: 10,
+ searchTerm: searchValue,
+ }
+ ),
+ {
+ enabled: searchValue.length > 0,
+ keepPreviousData: true,
+ }
+ );
+
+ const Organizations: { data: any; isLoading: boolean; refetch: any } =
+ useQuery([`fetch_orgs`], () => GraphQL(OrgList, {
+ [params.entityType]: params.entitySlug,
+ }, []));
+
+
+ const CollaborativeData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_collaborative_${params.id}`],
+ () =>
+ GraphQL(
+ FetchCollaborativeInfo,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: params.id,
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ useEffect(() => {
+ setFormData((prev) => ({
+ ...prev,
+ partners:
+ CollaborativeData?.data?.collaboratives?.[0]?.partnerOrganizations?.map(
+ (org: any) => ({
+ label: org.name,
+ value: org.id,
+ })
+ ) || [],
+ supporters:
+ CollaborativeData?.data?.collaboratives?.[0]?.supportingOrganizations?.map(
+ (org: any) => ({
+ label: org.name,
+ value: org.id,
+ })
+ ) || [],
+ contributors:
+ CollaborativeData?.data?.collaboratives?.[0]?.contributors?.map((user: any) => ({
+ label: user.fullName,
+ value: user.id,
+ })) || [],
+ }));
+ }, [CollaborativeData?.data]);
+
+ const { mutate: addContributor, isLoading: addContributorLoading } =
+ useMutation(
+ (input: { collaborativeId: string; userId: string }) =>
+ GraphQL(AddContributors, {
+ [params.entityType]: params.entitySlug,
+ }, input),
+ {
+ onSuccess: (res: any) => {
+ toast('Contributor added successfully');
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: removeContributor, isLoading: removeContributorLoading } =
+ useMutation(
+ (input: { collaborativeId: string; userId: string }) =>
+ GraphQL(RemoveContributor, {
+ [params.entityType]: params.entitySlug,
+ }, input),
+ {
+ onSuccess: (res: any) => {
+ toast('Contributor removed successfully');
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: addSupporter, isLoading: addSupporterLoading } = useMutation(
+ (input: { collaborativeId: string; organizationId: string }) =>
+ GraphQL(AddSupporters, {
+ [params.entityType]: params.entitySlug,
+ }, input),
+ {
+ onSuccess: (res: any) => {
+ toast('Supporter added successfully');
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: removeSupporter, isLoading: removeSupporterLoading } =
+ useMutation(
+ (input: { collaborativeId: string; organizationId: string }) =>
+ GraphQL(RemoveSupporters, {
+ [params.entityType]: params.entitySlug,
+ }, input),
+ {
+ onSuccess: (res: any) => {
+ toast('Supporter removed successfully');
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: addPartner, isLoading: addPartnerLoading } = useMutation(
+ (input: { collaborativeId: string; organizationId: string }) =>
+ GraphQL(AddPartners, {
+ [params.entityType]: params.entitySlug,
+ }, input),
+ {
+ onSuccess: (res: any) => {
+ toast('Partner added successfully');
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: removePartner, isLoading: removePartnerLoading } =
+ useMutation(
+ (input: { collaborativeId: string; organizationId: string }) =>
+ GraphQL(RemovePartners, {
+ [params.entityType]: params.entitySlug,
+ }, input),
+ {
+ onSuccess: (res: any) => {
+ toast('Partner removed successfully');
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ useEffect(() => {
+ Users.refetch();
+ }, [searchValue]);
+
+ const selectedContributors = formData.contributors;
+
+ const options =
+ Users?.data?.searchUsers?.map((user: any) => ({
+ label: user.fullName,
+ value: user.id,
+ })) || [];
+
+ const { setStatus } = useEditStatus();
+
+ const loadingStates = [
+ addContributorLoading,
+ removeContributorLoading,
+ addSupporterLoading,
+ removeSupporterLoading,
+ addPartnerLoading,
+ removePartnerLoading,
+ ];
+
+ useEffect(() => {
+ setStatus(loadingStates.some(Boolean) ? 'loading' : 'success');
+ }, loadingStates);
+
+ return (
+
+ {Users?.isLoading ||
+ Organizations?.data?.allOrganizations?.length === 0 ? (
+
+ ) : (
+
+
+
CONTRIBUTORS
+
+
+
+ Add Contributors
+ {
+ const prevValues = formData.contributors.map(
+ (item) => item.value
+ );
+ const newlyAdded = newValues.find(
+ (item: any) => !prevValues.includes(item.value)
+ );
+
+ setFormData((prev) => ({
+ ...prev,
+ contributors: newValues,
+ }));
+
+ if (newlyAdded) {
+ addContributor({
+ collaborativeId: params.id,
+ userId: newlyAdded.value,
+ });
+ }
+ setSearchValue(''); // clear input
+ }}
+ placeholder="Add Contributors"
+ onInput={(value: any) => {
+ setSearchValue(value);
+ }}
+ />
+
+
+ {formData.contributors.map((item) => (
+
+
contributor.id === item.value
+ )?.profilePicture?.url
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${
+ CollaborativeData.data.collaboratives[0]?.contributors?.find(
+ (contributor: any) =>
+ contributor.id === item.value
+ )?.profilePicture?.url
+ }`
+ : '/profile.png'
+ }
+ alt={item.label}
+ width={80}
+ height={80}
+ className="rounded-full object-cover"
+ />
+ {
+ setFormData((prev) => ({
+ ...prev,
+ contributors: prev.contributors.filter(
+ (contributor) => contributor.value !== item.value
+ ),
+ }));
+ removeContributor({
+ collaborativeId: params.id,
+ userId: item.value,
+ });
+ }}
+ kind="tertiary"
+ >
+
+ {item.label}
+
+
+
+
+ ))}
+
+
+
{' '}
+
+
+
({
+ label: org.name,
+ value: org.id,
+ })
+ )}
+ selectedValues={formData.supporters}
+ onChange={(newValues: any) => {
+ const prevValues = formData.supporters.map((item) => item.value);
+ const newlyAdded = newValues.find(
+ (item: any) => !prevValues.includes(item.value)
+ );
+
+ setFormData((prev) => ({ ...prev, supporters: newValues }));
+
+ if (newlyAdded) {
+ addSupporter({
+ collaborativeId: params.id,
+ organizationId: newlyAdded.value,
+ });
+ }
+ }}
+ onRemove={(item: any) => {
+ setFormData((prev) => ({
+ ...prev,
+ supporters: prev.supporters.filter(
+ (s) => s.value !== item.value
+ ),
+ }));
+ removeSupporter({
+ collaborativeId: params.id,
+ organizationId: item.value,
+ });
+ }}
+ />
+
+ ({
+ label: org.name,
+ value: org.id,
+ })
+ )}
+ selectedValues={formData.partners}
+ onChange={(newValues: any) => {
+ const prevValues = formData.partners.map((item) => item.value);
+ const newlyAdded = newValues.find(
+ (item: any) => !prevValues.includes(item.value)
+ );
+
+ setFormData((prev) => ({ ...prev, partners: newValues }));
+
+ if (newlyAdded) {
+ addPartner({
+ collaborativeId: params.id,
+ organizationId: newlyAdded.value,
+ });
+ }
+ }}
+ onRemove={(item: any) => {
+ setFormData((prev) => ({
+ ...prev,
+ partners: prev.partners.filter((s) => s.value !== item.value),
+ }));
+ removePartner({
+ collaborativeId: params.id,
+ organizationId: item.value,
+ });
+ }}
+ />
+
+ )}
+
+ );
+};
+
+export default Details;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts
new file mode 100644
index 00000000..ab68111f
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts
@@ -0,0 +1,189 @@
+import { graphql } from '@/gql';
+
+
+export const FetchUsers: any = graphql(`
+ query searchUsers($limit: Int!, $searchTerm: String!) {
+ searchUsers(limit: $limit, searchTerm: $searchTerm) {
+ id
+ fullName
+ username
+ }
+ }
+ `);
+
+export const FetchCollaborativeInfo: any = graphql(`
+ query collaborativeinfo($filters: CollaborativeFilter) {
+ collaboratives(filters: $filters) {
+ id
+ title
+ contributors {
+ id
+ fullName
+ username
+ profilePicture {
+ url
+ }
+ }
+ supportingOrganizations {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ partnerOrganizations{
+ id
+ name
+ logo{
+ url
+ name
+ }
+ }
+ }
+ }
+ `);
+
+export const AddContributors: any = graphql(`
+ mutation addContributorToCollaborative($collaborativeId: String!, $userId: ID!) {
+ addContributorToCollaborative(collaborativeId: $collaborativeId, userId: $userId) {
+ __typename
+ ... on TypeCollaborative {
+ id
+ title
+ contributors {
+ id
+ fullName
+ username
+ }
+ }
+ }
+ }
+ `);
+
+export const RemoveContributor: any = graphql(`
+ mutation removeContributorFromCollaborative($collaborativeId: String!, $userId: ID!) {
+ removeContributorFromCollaborative(collaborativeId: $collaborativeId, userId: $userId) {
+ __typename
+ ... on TypeCollaborative {
+ id
+ title
+ contributors {
+ id
+ fullName
+ username
+ }
+ }
+ }
+ }
+ `);
+
+export const AddSupporters: any = graphql(`
+ mutation addSupportingOrganizationToCollaborative(
+ $collaborativeId: String!
+ $organizationId: ID!
+ ) {
+ addSupportingOrganizationToCollaborative(
+ collaborativeId: $collaborativeId
+ organizationId: $organizationId
+ ) {
+ __typename
+ ... on TypeCollaborativeOrganizationRelationship {
+ organization {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ }
+ }
+ }
+ `);
+
+export const RemoveSupporters: any = graphql(`
+ mutation removeSupportingOrganizationFromCollaborative(
+ $collaborativeId: String!
+ $organizationId: ID!
+ ) {
+ removeSupportingOrganizationFromCollaborative(
+ collaborativeId: $collaborativeId
+ organizationId: $organizationId
+ ) {
+ __typename
+ ... on TypeCollaborativeOrganizationRelationship {
+ organization {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ }
+ }
+ }
+ `);
+
+export const AddPartners: any = graphql(`
+ mutation addPartnerOrganizationToCollaborative(
+ $collaborativeId: String!
+ $organizationId: ID!
+ ) {
+ addPartnerOrganizationToCollaborative(
+ collaborativeId: $collaborativeId
+ organizationId: $organizationId
+ ) {
+ __typename
+ ... on TypeCollaborativeOrganizationRelationship {
+ organization {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ }
+ }
+ }
+ `);
+
+export const RemovePartners: any = graphql(`
+ mutation removePartnerOrganizationFromCollaborative(
+ $collaborativeId: String!
+ $organizationId: ID!
+ ) {
+ removePartnerOrganizationFromCollaborative(
+ collaborativeId: $collaborativeId
+ organizationId: $organizationId
+ ) {
+ __typename
+ ... on TypeCollaborativeOrganizationRelationship {
+ organization {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ }
+ }
+ }
+ `);
+
+
+export const OrgList: any = graphql(`
+ query allOrgs {
+ allOrganizations {
+ id
+ name
+ logo {
+ path
+ url
+ }
+ }
+ }
+`);
\ No newline at end of file
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx
new file mode 100644
index 00000000..d2a4d4c8
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx
@@ -0,0 +1,323 @@
+'use client';
+
+import React, { useCallback, useEffect, useState } from 'react';
+import { useParams, useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { CollaborativeInputPartial } from '@/gql/generated/graphql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { DropZone, Select, TextField, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { useEditStatus } from '../../context';
+import Metadata from '../metadata/page';
+
+const UpdateCollaborativeMutation: any = graphql(`
+ mutation updateCollaborative($data: CollaborativeInputPartial!) {
+ updateCollaborative(data: $data) {
+ __typename
+ id
+ title
+ summary
+ created
+ modified
+ slug
+ status
+ startedOn
+ completedOn
+ platformUrl
+ logo {
+ name
+ path
+ url
+ }
+ coverImage {
+ name
+ path
+ url
+ }
+ }
+ }
+`);
+
+const FetchCollaborative: any = graphql(`
+ query CollaborativeData($filters: CollaborativeFilter) {
+ collaboratives(filters: $filters) {
+ id
+ title
+ summary
+ platformUrl
+ logo {
+ name
+ path
+ url
+ }
+ coverImage {
+ name
+ path
+ url
+ }
+ status
+ slug
+ startedOn
+ completedOn
+ }
+ }
+`);
+
+const Details = () => {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const router = useRouter();
+
+ const CollaborativeData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_CollaborativeData_details`],
+ () =>
+ GraphQL(
+ FetchCollaborative,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: params.id,
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const CollaborativesData =
+ CollaborativeData?.data?.collaboratives &&
+ Array.isArray(CollaborativeData?.data?.collaboratives) &&
+ CollaborativeData?.data?.collaboratives?.length > 0
+ ? CollaborativeData?.data?.collaboratives[0]
+ : null;
+
+ const initialFormData = {
+ title: '',
+ summary: '',
+ logo: null as File | null,
+ coverImage: null as File | null,
+ slug: '',
+ status: '',
+ startedOn: null,
+ completedOn: null,
+ platformUrl: '',
+ };
+
+ const [formData, setFormData] = useState(initialFormData);
+ const [previousFormData, setPreviousFormData] = useState(initialFormData);
+
+ useEffect(() => {
+ if (CollaborativesData) {
+ const updatedData = {
+ title: CollaborativesData.title || '',
+ summary: CollaborativesData.summary || '',
+ logo: CollaborativesData.logo || null,
+ coverImage: CollaborativesData.coverImage || null,
+ slug: CollaborativesData.slug || '',
+ status: CollaborativesData.status || '',
+ startedOn: CollaborativesData.startedOn || '',
+ completedOn: CollaborativesData.completedOn || '',
+ platformUrl: CollaborativesData.platformUrl || '',
+ };
+ setFormData(updatedData);
+ setPreviousFormData(updatedData);
+ }
+ }, [params.id, CollaborativesData]);
+
+ const { mutate, isLoading: editMutationLoading } = useMutation(
+ (data: { data: CollaborativeInputPartial }) =>
+ GraphQL(
+ UpdateCollaborativeMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ data
+ ),
+ {
+ onSuccess: (res: any) => {
+ toast('Collaborative updated successfully');
+ setFormData((prev) => ({
+ ...prev,
+ ...res.updateCollaborative,
+ }));
+ setPreviousFormData((prev) => ({
+ ...prev,
+ ...res.updateCollaborative,
+ }));
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const handleChange = useCallback((field: string, value: any) => {
+ setFormData((prevData) => ({
+ ...prevData,
+ [field]: value,
+ }));
+ }, []);
+
+ const onDrop = React.useCallback(
+ (_dropFiles: File[], acceptedFiles: File[]) => {
+ mutate({
+ data: {
+ id: params.id.toString(),
+ logo: acceptedFiles[0],
+ },
+ });
+ },
+ []
+ );
+
+ const onCoverImageDrop = React.useCallback(
+ (_dropFiles: File[], acceptedFiles: File[]) => {
+ mutate({
+ data: {
+ id: params.id.toString(),
+ coverImage: acceptedFiles[0],
+ },
+ });
+ },
+ []
+ );
+
+ const handleSave = (updatedData: any) => {
+ if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) {
+ setPreviousFormData(updatedData);
+
+ mutate({
+ data: {
+ id: params.id.toString(),
+ title: updatedData.title,
+ summary: updatedData.summary,
+ startedOn: (updatedData.startedOn as Date) || null,
+ completedOn: (updatedData.completedOn as Date) || null,
+ platformUrl: updatedData.platformUrl || '',
+ },
+ });
+ }
+ };
+
+ const { setStatus } = useEditStatus();
+
+ useEffect(() => {
+ setStatus(editMutationLoading ? 'loading' : 'success');
+ }, [editMutationLoading, setStatus]);
+
+ // Show loading state while fetching data
+ if (CollaborativeData.isLoading) {
+ return Loading collaborative data...
;
+ }
+
+ // Show error if no data found
+ if (!CollaborativesData) {
+ return No collaborative data found
;
+ }
+
+ return (
+
+
+ handleChange('summary', e)}
+ onBlur={() => handleSave(formData)}
+ />
+
+
+
+ handleChange('platformUrl', e)}
+ onBlur={() => handleSave(formData)}
+ />
+
+
+
+
+
+
+
+ {
+ handleChange('startedOn', e);
+ }}
+ onBlur={() => handleSave(formData)}
+ />
+
+
+
+ {
+ handleChange('completedOn', e);
+ }}
+ onBlur={() => handleSave(formData)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Details;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx
new file mode 100644
index 00000000..8768f960
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx
@@ -0,0 +1,517 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useParams } from 'next/navigation';
+import { graphql } from '@/gql';
+import { MetadataModels } from '@/gql/generated/graphql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Combobox, Spinner, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { useEditStatus } from '../../context';
+
+const FetchCollaborativeMetadata: any = graphql(`
+ query CollaborativeMetadata($filters: CollaborativeFilter) {
+ collaboratives(filters: $filters) {
+ id
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ tags {
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ sdgs {
+ id
+ code
+ name
+ }
+ geographies {
+ id
+ name
+ code
+ type
+ }
+ }
+ }
+`);
+
+const metadataQueryDoc = graphql(`
+ query CollaborativeMetaDataList($filters: MetadataFilter) {
+ metadata(filters: $filters) {
+ id
+ label
+ dataStandard
+ urn
+ dataType
+ options
+ filterable
+ }
+ }
+`);
+
+const sectorsListQueryDoc: any = graphql(`
+ query SectorList {
+ sectors {
+ id
+ name
+ }
+ }
+`);
+
+const sdgsListQueryDoc: any = graphql(`
+ query SDGList {
+ sdgs {
+ id
+ code
+ name
+ }
+ }
+`);
+
+const tagsListQueryDoc: any = graphql(`
+ query TagsList {
+ tags {
+ id
+ value
+ }
+ }
+`);
+
+const geographiesListQueryDoc: any = graphql(`
+ query GeographiesList {
+ geographies {
+ id
+ name
+ code
+ type
+ parentId {
+ id
+ name
+ }
+ }
+ }
+`);
+
+const UpdateCollaborativeMetadata: any = graphql(`
+ mutation addUpdateCollaborativeMetadata($updateMetadataInput: UpdateCollaborativeMetadataInput!) {
+ addUpdateCollaborativeMetadata(updateMetadataInput: $updateMetadataInput) {
+ __typename
+ ... on TypeCollaborative {
+ id
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ tags {
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ sdgs {
+ id
+ code
+ name
+ }
+ geographies {
+ id
+ name
+ code
+ type
+ }
+ }
+ }
+ }
+`);
+
+const Metadata = () => {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const { setStatus } = useEditStatus();
+
+ const collaborativeData: { data: any; isLoading: boolean } = useQuery(
+ [`fetch_CollaborativeData_Metadata`],
+ () =>
+ GraphQL(
+ FetchCollaborativeMetadata,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ { filters: { id: params.id } }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const { data: metadataFields, isLoading: isMetadataFieldsLoading } = useQuery(
+ [`metadata_fields_COLLABORATIVE_${params.id}`],
+ () =>
+ GraphQL(
+ metadataQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ model: 'COLLABORATIVE' as MetadataModels,
+ enabled: true,
+ },
+ }
+ )
+ );
+
+ const defaultValuesPrepFn = (data: any) => {
+ let defaultVal: {
+ [key: string]: any;
+ } = {};
+
+ data?.metadata?.map((field: any) => {
+ if (field.metadataItem.dataType === 'MULTISELECT' && field.value !== '') {
+ defaultVal[field.metadataItem.id] = field.value
+ .split(', ')
+ .map((value: string) => ({
+ label: value,
+ value: value,
+ }));
+ } else if (!field.value) {
+ defaultVal[field.metadataItem.id] = [];
+ } else {
+ defaultVal[field.metadataItem.id] = field.value;
+ }
+ });
+
+ defaultVal['sectors'] =
+ data?.sectors?.map((sector: any) => {
+ return {
+ label: sector.name,
+ value: sector.id,
+ };
+ }) || [];
+
+ defaultVal['sdgs'] =
+ data?.sdgs?.map((sdg: any) => {
+ return {
+ label: `${sdg.code} - ${sdg.name}`,
+ value: sdg.id,
+ };
+ }) || [];
+
+ defaultVal['tags'] =
+ data?.tags?.map((tag: any) => {
+ return {
+ label: tag.value,
+ value: tag.id,
+ };
+ }) || [];
+
+ defaultVal['geographies'] =
+ data?.geographies?.map((geo: any) => {
+ return {
+ label: geo.name,
+ value: geo.id,
+ };
+ }) || [];
+
+ return defaultVal;
+ };
+
+ const [formData, setFormData] = useState(
+ defaultValuesPrepFn(collaborativeData?.data?.collaboratives?.[0] || {})
+ );
+ const [previousFormData, setPreviousFormData] = useState(formData);
+
+ useEffect(() => {
+ if (collaborativeData.data?.collaboratives?.[0]) {
+ const updatedData = defaultValuesPrepFn(collaborativeData.data.collaboratives[0]);
+ setFormData(updatedData);
+ setPreviousFormData(updatedData);
+ }
+ }, [collaborativeData.data]);
+
+ const getSectorsList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`sectors_list_query`], () =>
+ GraphQL(
+ sectorsListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ []
+ )
+ );
+
+ const getSDGsList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`sdgs_list_query`], () =>
+ GraphQL(
+ sdgsListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ []
+ )
+ );
+
+ const getTagsList: {
+ data: any;
+ isLoading: boolean;
+ error: any;
+ refetch: any;
+ } = useQuery([`tags_list_query`], () =>
+ GraphQL(
+ tagsListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ []
+ )
+ );
+
+ const getGeographiesList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`geographies_list_query`], () =>
+ GraphQL(
+ geographiesListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ []
+ )
+ );
+ const [isTagsListUpdated, setIsTagsListUpdated] = useState(false);
+
+ // Update mutation
+ const updateCollaborative = useMutation(
+ (data: { updateMetadataInput: any }) =>
+ GraphQL(UpdateCollaborativeMetadata, {
+ [params.entityType]: params.entitySlug,
+ }, data),
+ {
+ onSuccess: (res: any) => {
+ toast('Collaborative updated successfully');
+ const updatedData = defaultValuesPrepFn(res.addUpdateCollaborativeMetadata);
+ if (isTagsListUpdated) {
+ getTagsList.refetch();
+ setIsTagsListUpdated(false);
+ }
+ setFormData(updatedData);
+ setPreviousFormData(updatedData);
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const handleChange = (field: string, value: any) => {
+ setFormData((prevData: any) => ({
+ ...prevData,
+ [field]: value,
+ }));
+ };
+
+ const handleSave = (updatedData: any) => {
+ if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) {
+ setPreviousFormData(updatedData);
+
+ updateCollaborative.mutate({
+ updateMetadataInput: {
+ id: params.id,
+ metadata: Object.keys(updatedData)
+ .filter(
+ (key) =>
+ !['tags', 'sectors', 'sdgs'].includes(key) &&
+ metadataFields?.metadata?.find((item: any) => item.id === key)
+ )
+ .map((key) => ({
+ id: key,
+ value: Array.isArray(updatedData[key])
+ ? updatedData[key].map((item: any) => item.value || item).join(', ')
+ : updatedData[key],
+ })),
+ sectors: updatedData.sectors?.map((item: any) => item.value) || [],
+ sdgs: updatedData.sdgs?.map((item: any) => item.value) || [],
+ tags: updatedData.tags?.map((item: any) => item.label) || [],
+ geographies: updatedData.geographies?.map((item: any) => parseInt(item.value, 10)) || [],
+ },
+ });
+ }
+ };
+
+ useEffect(() => {
+ setStatus(updateCollaborative.isLoading ? 'loading' : 'success');
+ }, [updateCollaborative.isLoading, setStatus]);
+
+ if (
+ getSectorsList.isLoading ||
+ getSDGsList.isLoading ||
+ getTagsList.isLoading ||
+ getGeographiesList.isLoading ||
+ collaborativeData.isLoading
+ ) {
+ return (
+
+
+
+ );
+ }
+
+ function renderInputField(metadataFormItem: any) {
+ if (metadataFormItem.dataType === 'SELECT') {
+ return (
+
+ ({
+ label: option,
+ value: option,
+ }))}
+ label={metadataFormItem.label}
+ selectedValue={formData[metadataFormItem.id]}
+ displaySelected
+ onChange={(value) => {
+ handleChange(metadataFormItem.id, value);
+ handleSave({ ...formData, [metadataFormItem.id]: value });
+ }}
+ />
+
+ );
+ }
+
+ if (metadataFormItem.dataType === 'MULTISELECT') {
+ return (
+
+ ({
+ label: option,
+ value: option,
+ })) || []),
+ ]}
+ label={metadataFormItem.label + ' *'}
+ selectedValue={formData[metadataFormItem.id]}
+ displaySelected
+ onChange={(value) => {
+ handleChange(metadataFormItem.id, value);
+ handleSave({ ...formData, [metadataFormItem.id]: value });
+ }}
+ />
+
+ );
+ }
+ }
+
+ return (
+
+
+
+
+ ({
+ label: `${item.code} - ${item.name}`,
+ value: item.id,
+ })) || []
+ }
+ selectedValue={formData.sdgs}
+ onChange={(value) => {
+ handleChange('sdgs', value);
+ handleSave({ ...formData, sdgs: value });
+ }}
+ />
+
+
+ ({
+ label: item.value,
+ value: item.id,
+ })) || []
+ }
+ key={`tags-${getTagsList.data?.tags?.length}`}
+ selectedValue={formData.tags}
+ onChange={(value) => {
+ setIsTagsListUpdated(true);
+ handleChange('tags', value);
+ handleSave({ ...formData, tags: value });
+ }}
+ />
+
+
+ ({
+ label: item.name,
+ value: item.id,
+ })) || []
+ }
+ selectedValue={formData.sectors}
+ onChange={(value) => {
+ handleChange('sectors', value);
+ handleSave({ ...formData, sectors: value });
+ }}
+ />
+
+
+ ({
+ label: `${item.name}${item.parentId ? ` (${item.parentId.name})` : ''}`,
+ value: item.id,
+ })) || []
+ }
+ selectedValue={formData.geographies}
+ onChange={(value) => {
+ handleChange('geographies', value);
+ handleSave({ ...formData, geographies: value });
+ }}
+ />
+
+
+
+
+ {metadataFields?.metadata?.map((item: any) =>
+ renderInputField(item)
+ )}
+
+
+
+ );
+};
+
+export default Metadata;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx
new file mode 100644
index 00000000..2866668f
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx
@@ -0,0 +1,32 @@
+import { Table } from 'opub-ui';
+
+import { formatDate } from '@/lib/utils';
+
+const Assign = ({ data }: { data: any }) => {
+ const columns = [
+ { accessorKey: 'title', header: 'Title' },
+ { accessorKey: 'sector', header: 'Sector' },
+ { accessorKey: 'modified', header: 'Last Modified' },
+ ];
+
+ const generateTableData = (list: Array) => {
+ return list?.map((item) => {
+ return {
+ title: item.title,
+ id: item.id,
+ sector: item.sectors[0]?.name,
+ modified: formatDate(item.modified),
+ };
+ });
+ };
+ return (
+
+ );
+};
+export default Assign;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx
new file mode 100644
index 00000000..e215c296
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx
@@ -0,0 +1,109 @@
+import Image from 'next/image';
+import { Text } from 'opub-ui';
+
+const Contributors = ({ data }: { data: any }) => {
+ const ContributorDetails = [
+ {
+ label: 'Contributors',
+ value:
+ data?.collaboratives[0]?.contributors.length > 0
+ ? data?.collaboratives[0]?.contributors
+ .map((item: any) => item.fullName)
+ .join(', ')
+ : 'No Contributors',
+ image: data?.collaboratives[0]?.contributors,
+ },
+ ];
+
+ const OrgDetails = [
+ {
+ label: 'Supporters',
+ value:
+ data?.collaboratives[0]?.supportingOrganizations.length > 0
+ ? data?.collaboratives[0]?.supportingOrganizations
+ .map((item: any) => item.name)
+ .join(', ')
+ : 'No Supporting Organizations',
+ image: data?.collaboratives[0]?.supportingOrganizations,
+ },
+ {
+ label: 'Partners',
+ value:
+ data?.collaboratives[0]?.partnerOrganizations.length > 0
+ ? data?.collaboratives[0]?.partnerOrganizations
+ .map((item: any) => item.name)
+ .join(', ')
+ : 'No Partner Organizations',
+ image: data?.collaboratives[0]?.partnerOrganizations,
+ },
+ ];
+ return (
+
+ {ContributorDetails.map((item: any, index: number) => (
+
+
+ {item.label}:
+
+
+ {item?.image.map((data: any, index: number) => (
+
+
+
+
+ {data.fullName}
+
+
+ ))}
+
+
+ ))}
+ {OrgDetails.map((item: any, index: number) => (
+
+
+ {item.label}:
+
+
+ {item.image.map((data: any, index: number) => (
+
+
+
+
+
+ {data.name}
+
+
+ ))}
+
+
+ ))}
+
+ );
+};
+
+export default Contributors;
\ No newline at end of file
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx
new file mode 100644
index 00000000..d97de6ac
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx
@@ -0,0 +1,106 @@
+import { useEffect, useState } from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { Text } from 'opub-ui';
+
+import { getWebsiteTitle } from '@/lib/utils';
+
+const Details = ({ data }: { data: any }) => {
+ const [platformTitle, setPlatformTitle] = useState(null);
+
+ useEffect(() => {
+ const fetchTitle = async () => {
+ try {
+ const urlItem = data.collaboratives[0].platformUrl;
+
+ if (urlItem && urlItem.value) {
+ const title = await getWebsiteTitle(urlItem.value);
+ setPlatformTitle(title);
+ }
+ } catch (error) {
+ console.error('Error fetching website title:', error);
+ }
+ };
+
+ if (data.collaboratives[0].platformUrl === null) {
+ setPlatformTitle('N/A');
+ } else {
+ fetchTitle();
+ }
+ }, [data?.collaboratives[0]?.platformUrl]);
+
+ const PrimaryDetails = [
+ { label: 'Collaborative Name', value: data?.collaboratives[0]?.title },
+ { label: 'Summary', value: data?.collaboratives[0]?.summary },
+ {
+ label: 'Running Status',
+ value: data?.collaboratives[0]?.runningStatus,
+ },
+ { label: 'Started On', value: data?.collaboratives[0]?.startedOn },
+ {
+ label: 'Completed On',
+ value: data?.collaboratives[0]?.completedOn,
+ },
+ { label: 'Sector', value: data?.collaboratives[0]?.sectors[0]?.name },
+ { label: 'Tags', value: data?.collaboratives[0]?.tags[0]?.value },
+ ...(data?.collaboratives[0]?.metadata?.map((meta: any) => ({
+ label: meta.metadataItem?.label,
+ value: meta.value,
+ })) || []),
+ ];
+ return (
+
+
+ <>
+ {PrimaryDetails.map(
+ (item, index) =>
+ item.value && (
+
+
+ {item.label}:
+
+
+ {item.value}
+
+
+ )
+ )}
+
+
+
+ Platform URL:
+
+
+
+
+ {platformTitle?.trim() ? platformTitle : 'Visit Platform'}
+
+
+
+
+
+ {data?.collaboratives[0]?.logo && (
+
+ )}
+ >
+
+
+ );
+};
+
+export default Details;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx
new file mode 100644
index 00000000..9b5bffe2
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx
@@ -0,0 +1,303 @@
+'use client';
+
+import { useParams, useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+ Button,
+ Icon,
+ Spinner,
+ Text,
+ toast,
+} from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { Icons } from '@/components/icons';
+import Assign from './Assign';
+import Contributors from './Contributors';
+import Details from './Details';
+
+const CollaborativeDetails: any = graphql(`
+ query CollabDetails($filters: CollaborativeFilter) {
+ collaboratives(filters: $filters) {
+ id
+ title
+ summary
+ website
+ platformUrl
+ metadata {
+ metadataItem {
+ id
+ label
+ dataType
+ }
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ sdgs {
+ id
+ code
+ name
+ }
+ tags {
+ id
+ value
+ }
+ startedOn
+ completedOn
+ logo {
+ name
+ path
+ url
+ }
+ coverImage {
+ name
+ path
+ url
+ }
+ datasets {
+ title
+ id
+ sectors {
+ name
+ }
+ modified
+ }
+ useCases {
+ title
+ id
+ slug
+ sectors {
+ name
+ }
+ modified
+ }
+ contactEmail
+ status
+ slug
+ contributors {
+ id
+ fullName
+ username
+ profilePicture {
+ url
+ }
+ }
+ supportingOrganizations {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ partnerOrganizations {
+ id
+ name
+ logo {
+ url
+ name
+ }
+ }
+ }
+ }
+`);
+
+const publishCollaborativeMutation: any = graphql(`
+ mutation publishCollaborative($collaborativeId: String!) {
+ publishCollaborative(collaborativeId: $collaborativeId) {
+ ... on TypeCollaborative {
+ id
+ status
+ }
+ }
+ }
+`);
+
+const Publish = () => {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+ const CollaborativeData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_CollaborativeDetails`],
+ () =>
+ GraphQL(
+ CollaborativeDetails,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: params.id,
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+ const router = useRouter();
+
+ const { mutate, isLoading: mutationLoading } = useMutation(
+ () => GraphQL(publishCollaborativeMutation, {
+ [params.entityType]: params.entitySlug,
+ }, { collaborativeId: params.id }),
+ {
+ onSuccess: (data: any) => {
+ toast('Collaborative Published Successfully');
+ router.push(
+ `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives`
+ );
+ },
+ onError: (err: any) => {
+ toast(`Received ${err} on dataset publish `);
+ },
+ }
+ );
+
+ const Summary = [
+ {
+ name: 'Details',
+ data: CollaborativeData.data?.collaboratives,
+ error:
+ CollaborativeData.data?.collaboratives[0]?.sectors.length === 0 ||
+ CollaborativeData.data?.collaboratives[0]?.summary.length === 0 ||
+ CollaborativeData.data?.collaboratives[0]?.sdgs.length === 0 ||
+ CollaborativeData.data?.collaboratives[0]?.logo === null
+ ? 'Summary or SDG or Sectors or Logo is missing. Please add to continue.'
+ : '',
+ errorType: 'critical',
+ },
+ {
+ name: 'Datasets',
+ data: CollaborativeData?.data?.collaboratives[0]?.datasets,
+ error:
+ CollaborativeData.data && CollaborativeData.data?.collaboratives[0]?.datasets.length === 0
+ ? 'No datasets assigned. Please assign to continue.'
+ : '',
+ },
+ {
+ name: 'Use Cases',
+ data: CollaborativeData?.data?.collaboratives[0]?.useCases,
+ error: '',
+ },
+ {
+ name: 'Dashboards',
+ data: CollaborativeData?.data?.collaboratives[0]?.length > 0,
+ error: '',
+ },
+ {
+ name: 'Contributors',
+ data: CollaborativeData?.data?.collaboratives[0]?.length > 0,
+ error: '',
+ },
+ ];
+
+ const isPublishDisabled = (collaborative: any) => {
+ if (!collaborative) return true;
+
+ const hasDatasets = collaborative?.datasets.length > 0;
+ const hasRequiredMetadata =
+ collaborative.sectors.length > 0 &&
+ collaborative?.summary.length > 0 &&
+ collaborative?.sdgs.length > 0 &&
+ collaborative?.logo !== null;
+
+ // No datasets assigned
+ if (!hasDatasets) return true;
+
+ // Required metadata check
+ if (!hasRequiredMetadata) return true;
+ };
+
+ return (
+ <>
+
+
+
+ REVIEW COLLABORATIVE DETAILS
+
+ :
+
+ Please check all the Collaborative details below before publishing
+
+
+
+ {CollaborativeData.isLoading || mutationLoading ? (
+
+
+
+ ) : (
+ <>
+ {Summary.map((item, index) => (
+
+
+
+
+
+ {item.name}
+
+ {item.error !== '' && (
+
+
+ {item.error}
+
+ )}
+
+
+
+
+ {item.name === 'Datasets' ? (
+
+ ) : item.name === 'Use Cases' ? (
+
+ ) : item.name === 'Details' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ ))}
+
mutate()}
+ disabled={isPublishDisabled(CollaborativeData?.data?.collaboratives[0])}
+ >
+ Publish
+
+ >
+ )}
+
+
+ >
+ );
+};
+
+export default Publish;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx
new file mode 100644
index 00000000..737f0384
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx
@@ -0,0 +1,180 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+import { useParams, useRouter } from 'next/navigation';
+import { fetchData } from '@/fetch';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Button, DataTable, Text, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { formatDate } from '@/lib/utils';
+import { Loading } from '@/components/loading';
+
+const FetchCollaborativeDetails: any = graphql(`
+ query CollaborativeUseCaseDetails($filters: CollaborativeFilter) {
+ collaboratives(filters: $filters) {
+ id
+ title
+ useCases {
+ id
+ title
+ slug
+ modified
+ sectors {
+ name
+ }
+ }
+ }
+ }
+`);
+
+const AssignCollaborativeUseCases: any = graphql(`
+ mutation assignCollaborativeUseCases($collaborativeId: String!, $useCaseIds: [String!]!) {
+ updateCollaborativeUseCases(collaborativeId: $collaborativeId, useCaseIds: $useCaseIds) {
+ ... on TypeCollaborative {
+ id
+ useCases {
+ id
+ title
+ }
+ }
+ }
+ }
+`);
+
+const UseCases = () => {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+ const router = useRouter();
+
+ const [data, setData] = useState([]); // Ensure `data` is an array
+ const [selectedRow, setSelectedRows] = useState([]);
+
+ const CollaborativeDetails: { data: any; isLoading: boolean; refetch: any } =
+ useQuery(
+ [`Collaborative_UseCase_Details`, params.id],
+ () =>
+ GraphQL(
+ FetchCollaborativeDetails,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: params.id,
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const formattedData = (data: any) =>
+ data.map((item: any) => {
+ return {
+ title: item.title,
+ id: item.id,
+ category: item.sectors[0]?.name || 'N/A', // Safeguard in case of missing category
+ modified: formatDate(item.modified),
+ };
+ });
+
+ useEffect(() => {
+ fetchData('usecase', '?size=1000&page=1')
+ .then((res) => {
+ setData(res.results);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }, []);
+
+ const columns = [
+ { accessorKey: 'title', header: 'Title' },
+ { accessorKey: 'category', header: 'Sector' },
+ { accessorKey: 'modified', header: 'Last Modified' },
+ ];
+
+ const generateTableData = (list: Array) => {
+ return list.map((item) => {
+ return {
+ title: item.title,
+ id: item.id,
+ category: item.sectors[0],
+ modified: formatDate(item.modified),
+ };
+ });
+ };
+
+ const { mutate, isLoading: mutationLoading } = useMutation(
+ () =>
+ GraphQL(
+ AssignCollaborativeUseCases,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ collaborativeId: params.id,
+ useCaseIds: Array.isArray(selectedRow)
+ ? selectedRow.map((row: any) => String(row.id))
+ : [],
+ }
+ ),
+ {
+ onSuccess: (data: any) => {
+ toast('Use Cases Assigned Successfully');
+ CollaborativeDetails.refetch();
+ router.push(
+ `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/contributors`
+ );
+ },
+ onError: (err: any) => {
+ toast(`Received ${err} on use case assignment`);
+ },
+ }
+ );
+
+ return (
+ <>
+ {CollaborativeDetails?.data?.collaboratives[0]?.useCases?.length >= 0 &&
+ data.length > 0 &&
+ !CollaborativeDetails.isLoading ? (
+ <>
+
+
+
+ Selected {selectedRow.length} of {data.length}
+
+
+
+ mutate()}>
+ Submit
+
+
+
+
+ {
+ setSelectedRows(Array.isArray(selected) ? selected : []); // Ensure selected is always an array
+ }}
+ />
+ >
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default UseCases;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/context.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/context.tsx
new file mode 100644
index 00000000..864e36c8
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/context.tsx
@@ -0,0 +1,27 @@
+'use client';
+import { createContext, useContext, useState } from 'react';
+
+type StatusType = 'loading' | 'success';
+
+const EditStatusContext = createContext<{
+ status: StatusType;
+ setStatus: (status: StatusType) => void;
+} | null>(null);
+
+export const EditStatusProvider = ({ children }: { children: React.ReactNode }) => {
+ const [status, setStatus] = useState('success');
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useEditStatus = () => {
+ const context = useContext(EditStatusContext);
+ if (!context) {
+ throw new Error('useEditStatus must be used within EditStatusProvider');
+ }
+ return context;
+};
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx
new file mode 100644
index 00000000..1c866e02
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx
@@ -0,0 +1,214 @@
+'use client';
+
+import { useParams, usePathname, useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { CollaborativeInputPartial } from '@/gql/generated/graphql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Tab, TabList, Tabs, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import StepNavigation from '../../components/StepNavigation';
+import TitleBar from '../../components/title-bar';
+import { EditStatusProvider, useEditStatus } from './context';
+
+const UpdateCollaborativeTitleMutation: any = graphql(`
+ mutation updateCollaborativeTitle($data: CollaborativeInputPartial!) {
+ updateCollaborative(data: $data) {
+ __typename
+ id
+ title
+ }
+ }
+`);
+
+const FetchCollaborativeTitle: any = graphql(`
+ query CollaborativeTitle($pk: ID!) {
+ collaborative(pk: $pk) {
+ id
+ title
+ }
+ }
+`);
+
+const TabsAndChildren = ({ children }: { children: React.ReactNode }) => {
+ const router = useRouter();
+ const pathName = usePathname();
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const layoutList = [
+ 'details',
+ 'contributors',
+ 'assign',
+ 'usecases',
+ 'publish',
+ ];
+
+ const pathItem = layoutList.find(function (v) {
+ return pathName.indexOf(v) >= 0;
+ });
+
+ const CollaborativeData: { data: any; isLoading: boolean; error: any; refetch: any } = useQuery(
+ [`fetch_CollaborativeData_${params.id}`],
+ () =>
+ GraphQL(
+ FetchCollaborativeTitle,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ pk: params.id,
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const { mutate, isLoading: editMutationLoading } = useMutation(
+ (data: { data: CollaborativeInputPartial }) =>
+ GraphQL(UpdateCollaborativeTitleMutation, {
+ [params.entityType]: params.entitySlug,
+ }, data),
+ {
+ onSuccess: () => {
+ toast('Collaborative updated successfully');
+ // Optionally, reset form or perform other actions
+ CollaborativeData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const links = [
+ {
+ label: 'Collaborative Details',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/details`,
+ selected: pathItem === 'details',
+ },
+ {
+ label: 'Datasets',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/assign`,
+ selected: pathItem === 'assign',
+ },
+ {
+ label: 'Use Cases',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/usecases`,
+ selected: pathItem === 'usecases',
+ },
+ {
+ label: 'Contributors',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/contributors`,
+ selected: pathItem === 'contributors',
+ },
+ {
+ label: 'Publish',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/publish`,
+ selected: pathItem === 'publish',
+ },
+ ];
+
+ const handleTabClick = (url: string) => {
+ router.replace(url); // Navigate to the selected tab
+ };
+
+ const initialTabLabel =
+ links.find((option) => option.selected)?.label || 'Collaborative Details';
+
+ const { status, setStatus } = useEditStatus();
+
+ // Debug logging
+ console.log('Layout - params:', params);
+ console.log('Layout - CollaborativeData:', CollaborativeData);
+ console.log('Layout - isLoading:', CollaborativeData.isLoading);
+ console.log('Layout - error:', CollaborativeData.error);
+ console.log('Layout - data:', CollaborativeData.data);
+
+ // Safely extract collaborative title - now using direct collaborative object
+ const collaborativeTitle = CollaborativeData?.data?.collaborative?.title || '';
+
+ console.log('Layout - collaborativeTitle:', collaborativeTitle);
+
+ // Show loading state while fetching
+ if (CollaborativeData.isLoading) {
+ return (
+
+
Loading collaborative data...
+
+ );
+ }
+
+ // Show error state if query failed
+ if (CollaborativeData.error) {
+ console.error('Collaborative query error:', CollaborativeData.error);
+ return (
+
+
Error loading collaborative data
+
+ {CollaborativeData.error?.message || 'Unknown error'}
+
+
+ Check console for details. ID: {params.id}
+
+
+ );
+ }
+
+ return (
+
+
mutate({ data: { title: e, id: params.id.toString() } })}
+ loading={editMutationLoading}
+ status={status}
+ setStatus={setStatus}
+ />
+
+ handleTabClick(
+ links.find((link) => link.label === newValue)?.url || ''
+ )
+ }
+ >
+
+ {links.map((item, index) => (
+ handleTabClick(item.url)}
+ className="uppercase"
+ >
+ {item.label}
+
+ ))}
+
+
+ {children}
+
+
+
+
+ );
+};
+
+const EditCollaborative = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default EditCollaborative;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx
new file mode 100644
index 00000000..b00b17a7
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx
@@ -0,0 +1,316 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { parseAsString, useQueryState } from 'next-usequerystate';
+import { Button, DataTable, Icon, IconButton, Text, toast } from 'opub-ui';
+import { twMerge } from 'tailwind-merge';
+
+import { GraphQL } from '@/lib/api';
+import { formatDate } from '@/lib/utils';
+import { Icons } from '@/components/icons';
+import { LinkButton } from '@/components/Link';
+import { Loading } from '@/components/loading';
+import { ActionBar } from '../dataset/components/action-bar';
+import { Navigation } from '../dataset/components/navigate-org-datasets';
+
+const allCollaboratives: any = graphql(`
+ query CollaborativesData($filters: CollaborativeFilter, $order: CollaborativeOrder) {
+ collaboratives(filters: $filters, order: $order) {
+ title
+ id
+ created
+ modified
+ }
+ }
+`);
+
+const deleteCollaborative: any = graphql(`
+ mutation deleteCollaborative($collaborativeId: String!) {
+ deleteCollaborative(collaborativeId: $collaborativeId)
+ }
+`);
+
+const AddCollaborative: any = graphql(`
+ mutation AddCollaborative {
+ addCollaborative {
+ __typename
+ ... on TypeCollaborative {
+ id
+ created
+ }
+ }
+ }
+`);
+
+const unPublishCollaborative: any = graphql(`
+ mutation unPublishCollaborativeMutation($collaborativeId: String!) {
+ unpublishCollaborative(collaborativeId: $collaborativeId) {
+ __typename
+ ... on TypeCollaborative {
+ id
+ title
+ created
+ }
+ }
+ }
+`);
+
+export default function CollaborativePage({
+ params,
+}: {
+ params: { entityType: string; entitySlug: string };
+}) {
+ const router = useRouter();
+
+ const [navigationTab, setNavigationTab] = useQueryState('tab', parseAsString);
+
+ let navigationOptions = [
+ {
+ label: 'Drafts',
+ url: `drafts`,
+ selected: navigationTab === 'drafts',
+ },
+ {
+ label: 'Published',
+ url: `published`,
+ selected: navigationTab === 'published',
+ },
+ ];
+
+ const AllCollaboratives: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_Collaboratives`],
+ () =>
+ GraphQL(
+ allCollaboratives,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ status: navigationTab === 'published' ? 'PUBLISHED' : 'DRAFT',
+ },
+ order: { modified: 'DESC' },
+ }
+ )
+ );
+
+ useEffect(() => {
+ if (navigationTab === null || navigationTab === undefined)
+ setNavigationTab('drafts');
+ AllCollaboratives.refetch();
+ }, [navigationTab]);
+
+ const DeleteCollaborativeMutation: {
+ mutate: any;
+ isLoading: boolean;
+ error: any;
+ } = useMutation(
+ [`delete_Collaborative`],
+ (data: { id: string }) =>
+ GraphQL(
+ deleteCollaborative,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ { collaborativeId: data.id }
+ ),
+ {
+ onSuccess: () => {
+ toast(`Deleted Collaborative successfully`);
+ AllCollaboratives.refetch();
+ },
+ onError: (err: any) => {
+ toast('Error: ' + err.message.split(':')[0]);
+ },
+ }
+ );
+
+ const CreateCollaborative: {
+ mutate: any;
+ isLoading: boolean;
+ error: any;
+ } = useMutation(
+ [`create_Collaborative`],
+ () =>
+ GraphQL(
+ AddCollaborative,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ []
+ ),
+ {
+ onSuccess: (response: any) => {
+ toast(`Collaborative created successfully`);
+ router.push(
+ `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${response.addCollaborative.id}/details`
+ );
+ AllCollaboratives.refetch();
+ },
+ onError: (err: any) => {
+ toast('Error: ' + err.message.split(':')[0]);
+ },
+ }
+ );
+
+ const UnpublishCollaborativeMutation: {
+ mutate: any;
+ isLoading: boolean;
+ error: any;
+ } = useMutation(
+ [`unpublish_collaborative`],
+ (data: { id: string }) =>
+ GraphQL(
+ unPublishCollaborative,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ { collaborativeId: data.id }
+ ),
+ {
+ onSuccess: () => {
+ toast(`Unpublished collaborative successfully`);
+ AllCollaboratives.refetch();
+ },
+ onError: (err: any) => {
+ toast('Error: ' + err.message.split(':')[0]);
+ },
+ }
+ );
+
+ const collaborativesListColumns = [
+ {
+ accessorKey: 'title',
+ header: 'Title',
+ cell: ({ row }: any) =>
+ navigationTab === 'published' ? (
+
+ {row.original.title}
+
+ ) : (
+
+
+ {row.original.title}
+
+
+ ),
+ },
+ { accessorKey: 'created', header: 'Date Created' },
+ { accessorKey: 'modified', header: 'Date Modified' },
+ {
+ accessorKey: 'delete',
+ header: 'Delete',
+ cell: ({ row }: any) =>
+ navigationTab === 'published' ? (
+ {
+ UnpublishCollaborativeMutation.mutate({
+ id: row.original?.id,
+ });
+ }}
+ >
+ Unpublish
+
+ ) : (
+ {
+ DeleteCollaborativeMutation.mutate({
+ id: row.original?.id,
+ });
+ }}
+ >
+ Delete
+
+ ),
+ },
+ ];
+
+ const generateTableData = (list: Array) => {
+ return list.map((item) => {
+ return {
+ title: item.title,
+ id: item.id,
+ created: formatDate(item.created),
+ modified: formatDate(item.modified),
+ };
+ });
+ };
+
+ return (
+ <>
+
+
+
+ {AllCollaboratives.data?.collaboratives.length > 0 ? (
+
+
item.selected)?.label || ''
+ }
+ primaryAction={{
+ content: 'Add New Collaborative',
+ onAction: () => CreateCollaborative.mutate(),
+ }}
+ />
+
+
+
+ ) : AllCollaboratives.isLoading ? (
+
+ ) : (
+ <>
+
+
+
+ {navigationTab === 'drafts' ? (
+ <>
+
+ You have not added any collaborative yet.
+
+ CreateCollaborative.mutate()}>
+ Add New Collaborative
+
+ >
+ ) : (
+
+ No Published Collaboratives yet.
+
+ )}
+
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx
index ef0c2790..f4ce8bb7 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx
@@ -79,6 +79,11 @@ export default function OrgDashboardLayout({ children }: DashboardLayoutProps) {
href: `/dashboard/${params.entityType}/${params.entitySlug}/usecases`,
icon: 'light',
},
+ {
+ title: 'Collaboratives',
+ href: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives`,
+ icon: 'userGroup',
+ },
{
title: 'Add & Manage Charts',
href: `/dashboard/${params.entityType}/${params.entitySlug}/charts`,
diff --git a/app/[locale]/dashboard/components/main-nav.tsx b/app/[locale]/dashboard/components/main-nav.tsx
index 963a3dc0..5735fd9b 100644
--- a/app/[locale]/dashboard/components/main-nav.tsx
+++ b/app/[locale]/dashboard/components/main-nav.tsx
@@ -107,6 +107,10 @@ export function MainNav({ hideSearch = false }) {
title: 'Use Cases',
href: '/usecases',
},
+ {
+ title: 'Collaboratives',
+ href: '/collaboratives',
+ },
{
title: 'Publishers',
href: '/publishers',
diff --git a/hooks/use-analytics.ts b/hooks/use-analytics.ts
index c684473a..2d1b9272 100644
--- a/hooks/use-analytics.ts
+++ b/hooks/use-analytics.ts
@@ -5,6 +5,7 @@ import {
trackUserInteraction,
trackDatasetView,
trackUsecaseView,
+ trackCollaborativeView,
trackSearch,
trackDownload,
trackEngagement,
@@ -37,6 +38,10 @@ export const useAnalytics = () => {
trackSearch(query, resultCount);
}, []);
+ const trackCollaborative = useCallback((collaborativeId: string, collaborativeTitle?: string) => {
+ trackCollaborativeView(collaborativeId, collaborativeTitle);
+ }, []);
+
const trackFileDownload = useCallback((fileName: string, fileType?: string) => {
trackDownload(fileName, fileType);
}, []);
@@ -59,6 +64,7 @@ export const useAnalytics = () => {
trackInteraction,
trackDataset,
trackUsecase,
+ trackCollaborative,
trackSearchQuery,
trackFileDownload,
trackUserEngagement,
diff --git a/lib/gtag.ts b/lib/gtag.ts
index 86d994e3..9ba82b16 100644
--- a/lib/gtag.ts
+++ b/lib/gtag.ts
@@ -125,3 +125,10 @@ export const trackScrollDepth = (scrollPercentage: number) => {
page_path: window.location.pathname,
});
};
+
+export const trackCollaborativeView = (collaborativeId: string, collaborativeTitle?: string) => {
+ trackEvent('collaborative_view', {
+ collaborative_id: collaborativeId,
+ collaborative_title: collaborativeTitle,
+ });
+};
\ No newline at end of file
diff --git a/public/collaborative.png b/public/collaborative.png
new file mode 100644
index 00000000..423de204
Binary files /dev/null and b/public/collaborative.png differ