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. + +
+
+ {'collaborative'} +
+
+
+
+ + + +
+
+ {/* 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" + /> +
+
+ +
+
+
+ + {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) && ( + + )} +
+ )} + + )} +
+
+
+ ); +}; + +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) => ( + +
+ {org.name} +
+ + ) + )} +
+
+ )} + {hasPartnerOrganizations && ( +
+ + Partnered by + +
+ {CollaborativeDetailsData?.collaborativeBySlug?.partnerOrganizations?.map( + (org: any) => ( + +
+ {org.name} +
+ + ) + )} +
+
+ )} +
+
+ )} +
+
+ {/* 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} + +
+ ))} +
+
+ + +
+ } + > + {isLoading ? ( +
+ +
+ ) : ( + + )} + +
+ {data.collaborativeBySlug.coverImage && ( +
+ {data.collaborativeBySlug.title} +
+ )} + + {/* 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) => ( + + {sector.name + + )) + ) : ( + 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) => ( + + {sdg.name + + )) + ) : ( + 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 && ( + + )} +
+
+ +
+
+ Collaborative logo +
+
+ {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) => ( + + + {contributor.fullName} + + + ))} +
+
+ )} +
+
+
+ ); +}; + +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) => ( - - {item - - )) + {data.useCase.sdgs && data.useCase.sdgs.length > 0 ? ( + data.useCase.sdgs.map((sdg: any, index: number) => ( + + {sdg.name + + )) ) : ( 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} + +
+
+ +
+
+ + { + 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" + /> +
+ +
+ ))} +
+
+
+
+); + +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" + /> + +
+ ))} +
+
+
{' '} +
+ + ({ + 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 && ( +
+
+ + Image: + +
+ +
+ )} + +
+
+ ); +}; + +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' ? ( +
+ ) : ( + + )} +
+
+
+
+ ))} + + + )} +
+
+ + ); +}; + +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} + +
+
+ +
+
+ + { + 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' ? ( + + ) : ( + { + 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. + + + + ) : ( + + 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