-
Summary
{
defaultVal['sdgs'] =
data?.sdgs?.map((sdg: any) => {
+ const num = String(sdg.number || 0).padStart(2, '0');
return {
- label: `${sdg.code} - ${sdg.name}`,
+ label: `${num}. ${sdg.name}`,
value: sdg.id,
};
}) || [];
@@ -433,10 +436,13 @@ const Metadata = () => {
label="SDG Goals *"
name="sdgs"
list={
- getSDGsList?.data.sdgs?.map((item: any) => ({
- label: `${item.code} - ${item.name}`,
- value: item.id,
- })) || []
+ getSDGsList?.data?.sdgs?.map((item: any) => {
+ const num = String(item.number || 0).padStart(2, '0');
+ return {
+ label: `${num}. ${item.name}`,
+ value: item.id,
+ };
+ }) || []
}
selectedValue={formData.sdgs}
onChange={(value) => {
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx
index 60f756d7..0abdf21b 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx
@@ -24,7 +24,6 @@ import {
} from 'opub-ui';
import { GraphQL } from '@/lib/api';
-import { Loading } from '@/components/loading';
import DatasetLoading from '../../../components/loading-dataset';
import { useDatasetEditStatus } from '../context';
@@ -46,6 +45,21 @@ const tagsListQueryDoc: any = graphql(`
}
`);
+const geographiesListQueryDoc: any = graphql(`
+ query GeographiesList {
+ geographies {
+ id
+ name
+ code
+ type
+ parentId {
+ id
+ name
+ }
+ }
+ }
+`);
+
const datasetMetadataQueryDoc: any = graphql(`
query MetadataValues($filters: DatasetFilter) {
datasets(filters: $filters) {
@@ -60,6 +74,12 @@ const datasetMetadataQueryDoc: any = graphql(`
id
name
}
+ geographies {
+ id
+ name
+ code
+ type
+ }
license
metadata {
metadataItem {
@@ -116,6 +136,12 @@ const updateMetadataMutationDoc: any = graphql(`
id
name
}
+ geographies {
+ id
+ name
+ code
+ type
+ }
license
accessType
metadata {
@@ -188,6 +214,17 @@ export function EditMetadata({ id }: { id: string }) {
)
);
+ const getGeographiesList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`geographies_list_query`], () =>
+ GraphQL(
+ geographiesListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ []
+ )
+ );
+
const getMetaDataListQuery: {
data: any;
isLoading: boolean;
@@ -250,13 +287,24 @@ export function EditMetadata({ id }: { id: string }) {
}
);
- const defaultValuesPrepFn = (dataset: TypeDataset) => {
+ const defaultValuesPrepFn = (dataset?: TypeDataset) => {
let defaultVal: {
[key: string]: any;
} = {};
- dataset?.metadata.length > 0 &&
- dataset?.metadata?.map((field) => {
+ if (!dataset) {
+ return {
+ description: '',
+ sectors: [],
+ license: null,
+ tags: [],
+ geographies: [],
+ isPublic: true,
+ };
+ }
+
+ (dataset?.metadata || []).length > 0 &&
+ (dataset?.metadata || []).map((field) => {
if (
field.metadataItem.dataType === 'MULTISELECT' &&
field.value !== ''
@@ -294,13 +342,21 @@ export function EditMetadata({ id }: { id: string }) {
};
}) || [];
+ defaultVal['geographies'] =
+ dataset?.geographies?.map((geo: any) => {
+ return {
+ label: geo.name,
+ value: geo.id,
+ };
+ }) || [];
+
defaultVal['isPublic'] = true;
return defaultVal;
};
const [formData, setFormData] = useState(
- defaultValuesPrepFn(getDatasetMetadata?.data?.datasets[0])
+ defaultValuesPrepFn(getDatasetMetadata?.data?.datasets?.[0] || {} as TypeDataset)
);
const [previousFormData, setPreviousFormData] = useState(formData);
@@ -372,6 +428,7 @@ export function EditMetadata({ id }: { id: string }) {
'sectors',
'description',
'tags',
+ 'geographies',
'isPublic',
'license',
].includes(key) && transformedValues[key] !== ''
@@ -393,6 +450,9 @@ export function EditMetadata({ id }: { id: string }) {
...(changedFields.sectors && {
sectors: changedFields.sectors.map((item: any) => item.value),
}),
+ ...(changedFields.geographies && {
+ geographies: changedFields.geographies.map((item: any) => parseInt(item.value, 10)),
+ }),
},
});
};
@@ -530,6 +590,7 @@ export function EditMetadata({ id }: { id: string }) {
<>
{!getTagsList?.isLoading &&
!getSectorsList?.isLoading &&
+ !getGeographiesList?.isLoading &&
!getDatasetMetadata.isLoading ? (
+ ({
+ label: `${item.name}${item.parentId ? ` (${item.parentId.name})` : ''}`,
+ value: item.id,
+ })) || []
+ }
+ selectedValue={formData.geographies}
+ onChange={(value) => {
+ handleChange('geographies', value);
+ handleSave({ ...formData, geographies: value });
+ }}
+ />
{getMetaDataListQuery?.data?.metadata
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx
index 7c0bfc0f..705ecf84 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx
@@ -1,18 +1,17 @@
'use client';
-import { useEffect, useState } from 'react';
-import { useParams } from 'next/navigation';
import { graphql } from '@/gql';
import {
MetadataModels,
TypeMetadata,
TypeSector,
TypeTag,
- TypeUseCase,
UpdateUseCaseMetadataInput,
} from '@/gql/generated/graphql';
import { useMutation, useQuery } from '@tanstack/react-query';
+import { useParams } from 'next/navigation';
import { Combobox, Spinner, toast } from 'opub-ui';
+import { useEffect, useState } from 'react';
import { GraphQL } from '@/lib/api';
import { useEditStatus } from '../../context';
@@ -30,39 +29,62 @@ const FetchUseCasedetails: any = graphql(`
id
value
}
+ tags {
+ id
+ value
+ }
sectors {
id
name
}
- tags {
+ geographies {
id
- value
+ name
+ code
+ type
+ }
+ sdgs {
+ id
+ code
+ name
+ number
}
}
}
`);
const UpdateUseCaseMetadataMutation: any = graphql(`
- mutation updateUsecase($updateMetadataInput: UpdateUseCaseMetadataInput!) {
+ mutation addUpdateUsecaseMetadata($updateMetadataInput: UpdateUseCaseMetadataInput!) {
addUpdateUsecaseMetadata(updateMetadataInput: $updateMetadataInput) {
... on TypeUseCase {
- id
- metadata {
- metadataItem {
- id
- label
- dataType
- }
- id
- value
- }
- sectors {
+ id
+ metadata {
+ metadataItem {
id
- name
+ label
+ dataType
}
- tags {
- id
- value
+ id
+ value
+ }
+ tags {
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ geographies {
+ id
+ name
+ code
+ type
+ }
+ sdgs {
+ id
+ code
+ name
}
}
}
@@ -95,6 +117,21 @@ const sectorsListQueryDoc: any = graphql(`
}
`);
+const geographiesListQueryDoc: any = graphql(`
+ query GeographiesList {
+ geographies {
+ id
+ name
+ code
+ type
+ parentId {
+ id
+ name
+ }
+ }
+ }
+`);
+
const tagsListQueryDoc: any = graphql(`
query TagsList {
tags {
@@ -104,6 +141,17 @@ const tagsListQueryDoc: any = graphql(`
}
`);
+const sdgsListQueryDoc: any = graphql(`
+ query SDGList {
+ sdgs {
+ id
+ code
+ name
+ number
+ }
+ }
+`);
+
const Metadata = () => {
const params = useParams<{
entityType: string;
@@ -128,7 +176,7 @@ const Metadata = () => {
refetchOnReconnect: true,
}
);
- const { data: metadataFields, isLoading: isMetadataFieldsLoading } = useQuery(
+ const { data: metadataFields } = useQuery(
[`metadata_fields_USECASE_${params.id}`],
() =>
GraphQL(
@@ -145,12 +193,21 @@ const Metadata = () => {
)
);
- const defaultValuesPrepFn = (data: TypeUseCase) => {
+ const defaultValuesPrepFn = (data: any) => {
let defaultVal: {
[key: string]: any;
} = {};
- data?.metadata?.map((field) => {
+ if (!data) {
+ return {
+ sectors: [],
+ geographies: [],
+ tags: [],
+ sdgs: [],
+ };
+ }
+
+ data?.metadata?.map((field: any) => {
if (field.metadataItem.dataType === 'MULTISELECT' && field.value !== '') {
defaultVal[field.metadataItem.id] = field.value
.split(', ')
@@ -173,6 +230,23 @@ const Metadata = () => {
};
}) || [];
+ defaultVal['geographies'] =
+ data?.geographies?.map((geo: any) => {
+ return {
+ label: geo.name,
+ value: geo.id,
+ };
+ }) || [];
+
+ defaultVal['sdgs'] =
+ data?.sdgs?.map((sdg: any) => {
+ const num = String(sdg.number || 0).padStart(2, '0');
+ return {
+ label: `${num}. ${sdg.name}`,
+ value: sdg.id,
+ };
+ }) || [];
+
defaultVal['tags'] =
data?.tags?.map((tag: TypeTag) => {
return {
@@ -185,7 +259,7 @@ const Metadata = () => {
};
const [formData, setFormData] = useState(
- defaultValuesPrepFn(useCaseData?.data?.useCases[0])
+ defaultValuesPrepFn(useCaseData?.data?.useCases?.[0] || {})
);
const [previousFormData, setPreviousFormData] = useState(formData);
@@ -208,6 +282,28 @@ const Metadata = () => {
)
);
+ const getGeographiesList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`geographies_list_query`], () =>
+ GraphQL(
+ geographiesListQueryDoc,
+ {
+ [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;
@@ -279,7 +375,7 @@ const Metadata = () => {
...Object.keys(transformedValues)
.filter(
(valueItem) =>
- !['sectors', 'tags'].includes(valueItem) &&
+ !['sectors', 'tags', 'geographies', 'sdgs'].includes(valueItem) &&
transformedValues[valueItem] !== ''
)
.map((key) => {
@@ -291,6 +387,8 @@ const Metadata = () => {
],
sectors: updatedData.sectors?.map((item: any) => item.value) || [],
tags: updatedData.tags?.map((item: any) => item.label) || [],
+ sdgs: updatedData.sdgs?.map((item: any) => item.value) || [],
+ geographies: updatedData.geographies?.map((item: any) => parseInt(item.value, 10)) || [],
},
});
}
@@ -299,6 +397,8 @@ const Metadata = () => {
if (
getSectorsList.isLoading ||
getTagsList.isLoading ||
+ getSDGsList.isLoading ||
+ getGeographiesList.isLoading ||
useCaseData.isLoading
) {
return (
@@ -358,6 +458,27 @@ const Metadata = () => {
+
+ {
+ const num = String(item.number || 0).padStart(2, '0');
+ return {
+ label: `${num}. ${item.name}`,
+ value: item.id,
+ };
+ }) || []
+ }
+ selectedValue={formData.sdgs}
+ onChange={(value) => {
+ handleChange('sdgs', value);
+ handleSave({ ...formData, sdgs: 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 });
+ }}
+ />
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx
index ac096633..95d780a4 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx
@@ -27,7 +27,7 @@ const Details = ({ data }: { data: any }) => {
} else {
fetchTitle();
}
- }, [data?.useCases[0]?.platformUrl]);
+ }, [data?.useCases, data?.useCases[0]?.platformUrl]);
const PrimaryDetails = [
{ label: 'Use Case Name', value: data?.useCases[0]?.title },
@@ -42,6 +42,8 @@ const Details = ({ data }: { data: any }) => {
value: data?.useCases[0]?.completedOn,
},
{ label: 'Sector', value: data?.useCases[0]?.sectors[0]?.name },
+ { label: 'Geography', value: data?.useCases[0]?.geographies?.map((geo: any) => geo.name).join(', ') },
+ { label: 'SDG Goals', value: data?.useCases[0]?.sdgs?.map((sdg: any) => `${sdg.code} - ${sdg.name}`).join(', ') },
{ label: 'Tags', value: data?.useCases[0]?.tags[0]?.value },
...(data?.useCases[0]?.metadata?.map((meta: any) => ({
label: meta.metadataItem?.label,
@@ -71,14 +73,18 @@ const Details = ({ data }: { data: any }) => {
Platform URL:
-
-
- {platformTitle?.trim() ? platformTitle : 'Visit Platform'}
-
-
+ {data.useCases[0].platformUrl ? (
+
+
+ {platformTitle?.trim() ? platformTitle : 'Visit Platform'}
+
+
+ ) : (
+ Not provided
+ )}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx
index 41a36310..dc7aaeef 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx
@@ -43,6 +43,17 @@ const UseCaseDetails: any = graphql(`
id
name
}
+ geographies {
+ id
+ name
+ code
+ type
+ }
+ sdgs {
+ id
+ code
+ name
+ }
runningStatus
tags {
id
@@ -142,7 +153,7 @@ const Publish = () => {
[params.entityType]: params.entitySlug,
}, { useCaseId: params.id }),
{
- onSuccess: (data: any) => {
+ onSuccess: () => {
toast('UseCase Published Successfully');
router.push(
`/dashboard/${params.entityType}/${params.entitySlug}/usecases`
@@ -161,7 +172,7 @@ const Publish = () => {
error:
UseCaseData.data?.useCases[0]?.sectors.length === 0 ||
UseCaseData.data?.useCases[0]?.summary.length === 0 ||
- UseCaseData.data?.useCases[0]?.metadata.length === 0 ||
+ UseCaseData.data?.useCases[0]?.sdgs.length === 0 ||
UseCaseData.data?.useCases[0]?.logo === null
? 'Summary or SDG or Sectors or Logo is missing. Please add to continue.'
: '',
@@ -194,7 +205,7 @@ const Publish = () => {
const hasRequiredMetadata =
useCase.sectors.length > 0 &&
useCase?.summary.length > 0 &&
- useCase?.metadata.length > 0 &&
+ useCase?.sdgs.length > 0 &&
useCase?.logo !== null;
// No datasets assigned
diff --git a/app/[locale]/dashboard/[entityType]/page.tsx b/app/[locale]/dashboard/[entityType]/page.tsx
index 54d4e469..c25d6386 100644
--- a/app/[locale]/dashboard/[entityType]/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/page.tsx
@@ -55,7 +55,7 @@ const Page = () => {
const [formData, setFormData] = useState(initialFormData);
- const { mutate, isLoading: editMutationLoading } = useMutation(
+ const { mutate } = useMutation(
(input: { input: OrganizationInput }) =>
GraphQL(organizationCreationMutation, {}, input),
{
diff --git a/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx b/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx
index 9c4b5a0f..86a96714 100644
--- a/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx
+++ b/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx
@@ -39,13 +39,6 @@ const Footer: React.FC
= ({
}
};
- const handleJumpToPage = (event: React.ChangeEvent) => {
- const pageNumber = parseInt(event.target.value);
- if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= totalPages) {
- onPageChange(pageNumber);
- }
- };
-
const handlePageSizeChange = (event: any) => {
const newSize = parseInt(event as string);
if (!isNaN(newSize) && newSize > 0) {
diff --git a/app/[locale]/dashboard/components/main-nav.tsx b/app/[locale]/dashboard/components/main-nav.tsx
index 5735fd9b..b0a7b139 100644
--- a/app/[locale]/dashboard/components/main-nav.tsx
+++ b/app/[locale]/dashboard/components/main-nav.tsx
@@ -88,7 +88,7 @@ export function MainNav({ hideSearch = false }) {
};
fetchData();
- }, [session, hasFetched]);
+ }, [session, hasFetched, setUserDetails, setAllEntityDetails]);
if (isLoggingOut) {
return ;
diff --git a/app/[locale]/dashboard/page.tsx b/app/[locale]/dashboard/page.tsx
index 530e9e1f..89db6085 100644
--- a/app/[locale]/dashboard/page.tsx
+++ b/app/[locale]/dashboard/page.tsx
@@ -9,7 +9,7 @@ import { Loading } from '@/components/loading';
import { useDashboardStore } from '@/config/store';
const UserDashboard = () => {
- const { userDetails, allEntityDetails } = useDashboardStore();
+ const { userDetails } = useDashboardStore();
const list = [
{
label: 'My Dashboard',
diff --git a/app/api/auth/[...nextauth]/options.ts b/app/api/auth/[...nextauth]/options.ts
index 58755cde..4810ca17 100644
--- a/app/api/auth/[...nextauth]/options.ts
+++ b/app/api/auth/[...nextauth]/options.ts
@@ -4,8 +4,6 @@ import { Account, AuthOptions, Session } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import KeycloakProvider from 'next-auth/providers/keycloak';
-import { encrypt } from '@/lib/encryption';
-
// this will refresh an expired access token, when needed
async function refreshAccessToken(token: JWT) {
const urlObj: Record = {
diff --git a/components/SessionGuard.tsx b/components/SessionGuard.tsx
index fa9622cd..397b7a16 100644
--- a/components/SessionGuard.tsx
+++ b/components/SessionGuard.tsx
@@ -16,7 +16,7 @@ export default function SessionGuard({ children }: { children: ReactNode }) {
) {
signIn('keycloak');
}
- }, [data]);
+ }, [data, pathname]);
return <>{children}>;
}
diff --git a/components/ui/tree-view.tsx b/components/ui/tree-view.tsx
new file mode 100644
index 00000000..cc5c6a4f
--- /dev/null
+++ b/components/ui/tree-view.tsx
@@ -0,0 +1,149 @@
+import * as React from "react"
+import { ChevronRight } from "lucide-react"
+import { cn } from "@/lib/utils"
+
+interface TreeDataItem {
+ id: string
+ name: string
+ children?: TreeDataItem[]
+ [key: string]: any
+}
+
+interface TreeViewProps {
+ data: TreeDataItem[]
+ selectedItems?: string[]
+ onSelectChange?: (items: string[]) => void
+ expandedItems?: string[]
+ onExpandedChange?: (items: string[]) => void
+ className?: string
+}
+
+interface TreeItemProps {
+ item: TreeDataItem
+ level: number
+ selectedItems: string[]
+ expandedItems: string[]
+ onSelectChange: (items: string[]) => void
+ onExpandedChange: (items: string[]) => void
+}
+
+const TreeItem: React.FC = ({
+ item,
+ level,
+ selectedItems,
+ expandedItems,
+ onSelectChange,
+ onExpandedChange,
+}) => {
+ const hasChildren = item.children && item.children.length > 0
+ const isExpanded = expandedItems.includes(item.id)
+ const isSelected = selectedItems.includes(item.id)
+
+ const handleToggle = () => {
+ if (hasChildren) {
+ if (isExpanded) {
+ onExpandedChange(expandedItems.filter((id) => id !== item.id))
+ } else {
+ onExpandedChange([...expandedItems, item.id])
+ }
+ }
+ }
+
+ const handleSelect = (e: React.ChangeEvent) => {
+ e.stopPropagation()
+ if (isSelected) {
+ onSelectChange(selectedItems.filter((id) => id !== item.id))
+ } else {
+ onSelectChange([...selectedItems, item.id])
+ }
+ }
+
+ return (
+
+
+
+ {hasChildren ? (
+
+ ) : (
+
+ )}
+
+
e.stopPropagation()}
+ />
+
+
+ {item.name}
+
+
+
+
+ {hasChildren && isExpanded && (
+
+ {item.children!.map((child) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+export const TreeView: React.FC = ({
+ data,
+ selectedItems = [],
+ onSelectChange = () => {},
+ expandedItems = [],
+ onExpandedChange = () => {},
+ className,
+}) => {
+ return (
+
+ {data.map((item) => (
+
+ ))}
+
+ )
+}
diff --git a/package-lock.json b/package-lock.json
index 8997dbfb..1b52d6e1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"gtag": "^1.0.1",
"holy-loader": "^2.2.10",
"jwt-decode": "^4.0.0",
+ "lucide-react": "^0.544.0",
"next": "^14.0.4",
"next-auth": "^4.24.7",
"next-intl": "^3.4.0",
@@ -17154,6 +17155,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.544.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
+ "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
diff --git a/package.json b/package.json
index 23177209..289dcbb9 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"gtag": "^1.0.1",
"holy-loader": "^2.2.10",
"jwt-decode": "^4.0.0",
+ "lucide-react": "^0.544.0",
"next": "^14.0.4",
"next-auth": "^4.24.7",
"next-intl": "^3.4.0",
diff --git a/public/collaborative.svg b/public/collaborative.svg
new file mode 100644
index 00000000..8e466388
--- /dev/null
+++ b/public/collaborative.svg
@@ -0,0 +1,9 @@
+