diff --git a/apps/dataset-browser/src/app/[locale]/layout.tsx b/apps/dataset-browser/src/app/[locale]/layout.tsx
index 40718f681..cd7c3738f 100644
--- a/apps/dataset-browser/src/app/[locale]/layout.tsx
+++ b/apps/dataset-browser/src/app/[locale]/layout.tsx
@@ -16,7 +16,7 @@ export default async function RootLayout({children, params: {locale}}: Props) {
let messages;
try {
messages = (await import(`../../messages/${locale}/messages.json`)).default;
- } catch (error) {
+ } catch (err) {
notFound();
}
diff --git a/apps/dataset-browser/src/app/[locale]/page.tsx b/apps/dataset-browser/src/app/[locale]/page.tsx
index 2a85394f3..89850a553 100644
--- a/apps/dataset-browser/src/app/[locale]/page.tsx
+++ b/apps/dataset-browser/src/app/[locale]/page.tsx
@@ -97,9 +97,9 @@ export default async function Home({searchParams = {}}: Props) {
let searchResult: SearchResult | undefined;
try {
searchResult = await datasetFetcher.search(searchOptions);
- } catch (error) {
+ } catch (err) {
hasError = true;
- console.error(error);
+ console.error(err);
}
const locale = useLocale();
diff --git a/apps/researcher/package.json b/apps/researcher/package.json
index 27503e0cb..ec6ba3190 100644
--- a/apps/researcher/package.json
+++ b/apps/researcher/package.json
@@ -31,6 +31,7 @@
"@hapi/hoek": "11.0.2",
"@headlessui/react": "1.7.17",
"@heroicons/react": "2.0.18",
+ "@hookform/resolvers": "3.3.1",
"@next/mdx": "13.5.3",
"classnames": "2.3.2",
"fetch-sparql-endpoint": "3.3.3",
@@ -40,6 +41,8 @@
"rdf-object": "1.14.0",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "7.46.1",
+ "tiny-case": "1.0.3",
"zod": "3.22.2",
"zustand": "4.3.9"
},
diff --git a/apps/researcher/src/app/[locale]/communities/[slug]/[listId]/page.tsx b/apps/researcher/src/app/[locale]/communities/[slug]/[listId]/page.tsx
new file mode 100644
index 000000000..52f9f19ff
--- /dev/null
+++ b/apps/researcher/src/app/[locale]/communities/[slug]/[listId]/page.tsx
@@ -0,0 +1,7 @@
+export default function Page() {
+ return (
+
+
This page is under construction
+
+ );
+}
diff --git a/apps/researcher/src/app/[locale]/communities/[slug]/buttons.tsx b/apps/researcher/src/app/[locale]/communities/[slug]/buttons.tsx
index 20d4dfddc..ceca4b7a4 100644
--- a/apps/researcher/src/app/[locale]/communities/[slug]/buttons.tsx
+++ b/apps/researcher/src/app/[locale]/communities/[slug]/buttons.tsx
@@ -38,10 +38,10 @@ export function JoinCommunityButton({communityId}: Props) {
organizationId: communityId,
userId: user!.id,
});
- } catch (error) {
+ } catch (err) {
setIsClicked(false);
setHasError(true);
- console.error(error);
+ console.error(err);
}
});
};
diff --git a/apps/researcher/src/app/[locale]/communities/[slug]/object.tsx b/apps/researcher/src/app/[locale]/communities/[slug]/object.tsx
new file mode 100644
index 000000000..1bd6c6c28
--- /dev/null
+++ b/apps/researcher/src/app/[locale]/communities/[slug]/object.tsx
@@ -0,0 +1,19 @@
+import heritageObjects from '@/lib/heritage-objects-instance';
+
+interface Props {
+ objectIri: string;
+}
+
+export default async function ObjectCard({objectIri}: Props) {
+ const object = await heritageObjects.getById(objectIri);
+
+ if (!object) {
+ return null;
+ }
+
+ return (
+
+ {object.name}
+
+ );
+}
diff --git a/apps/researcher/src/app/[locale]/communities/[slug]/page.tsx b/apps/researcher/src/app/[locale]/communities/[slug]/page.tsx
index a9c50e9ef..17008e28f 100644
--- a/apps/researcher/src/app/[locale]/communities/[slug]/page.tsx
+++ b/apps/researcher/src/app/[locale]/communities/[slug]/page.tsx
@@ -7,6 +7,10 @@ import {getMemberships, getCommunityBySlug, isAdmin} from '@/lib/community';
import ErrorMessage from '@/components/error-message';
import {ClerkAPIResponseError} from '@clerk/shared';
import {revalidatePath} from 'next/cache';
+import {objectList} from '@colonial-collections/database';
+import ObjectCard from './object';
+import AddObjectListForm from '@/components/add-object-list-form';
+import {SlideOutButton, SlideOut, Notifications} from 'ui';
interface Props {
params: {
@@ -15,6 +19,8 @@ interface Props {
};
}
+const slideOutFormId = 'add-object-list';
+
export default async function CommunityPage({params}: Props) {
const t = await getTranslator(params.locale, 'Community');
@@ -39,9 +45,19 @@ export default async function CommunityPage({params}: Props) {
return ;
}
+ let objectLists;
+ try {
+ objectLists = await objectList.getByCommunityId(community.id, {
+ withObjects: true,
+ limitObjects: 4,
+ });
+ } catch (err) {
+ return ;
+ }
+
return (
<>
-
+
@@ -52,27 +68,93 @@ export default async function CommunityPage({params}: Props) {
{isAdmin(memberships) && }
-
-
- {t('title')}
-
- {community.name}
-
-
-
-
-
-
-
- {/*Place the description here*/}
+
+
+
+
+
-
-
+
+
+
+ {t('title')}
+
+ {community.name}
+
+
+
+
+
+ {/*Place the description here*/}
+
+
+
+
-
{t('objectListsTitle')}
-
- {/*Place the lists here */}
+
+
+
+
+
{t('objectListsTitle')}
+
{t('objectListsSubTitle', {count: objectLists.length})}
+
+
+ {isAdmin(memberships) && (
+
+ {t('addObjectListButton')}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {objectLists.map(objectList => (
+
+
+ {objectList.name}
+
+
{objectList.description}
+
+
+
+ {objectList.objects?.map(object => (
+
+ ))}
+
+
+
+
+
+
+
+ ))}
+
-
{/* TODO add number of lists here */}
+
+
+
+
+
);
diff --git a/apps/researcher/src/app/[locale]/layout.tsx b/apps/researcher/src/app/[locale]/layout.tsx
index ab228a59e..cd97f60d2 100644
--- a/apps/researcher/src/app/[locale]/layout.tsx
+++ b/apps/researcher/src/app/[locale]/layout.tsx
@@ -17,7 +17,7 @@ export default async function RootLayout({children, params: {locale}}: Props) {
let messages;
try {
messages = (await import(`../../messages/${locale}/messages.json`)).default;
- } catch (error) {
+ } catch (err) {
notFound();
}
diff --git a/apps/researcher/src/app/[locale]/page.tsx b/apps/researcher/src/app/[locale]/page.tsx
index dc8852154..4931fc85b 100644
--- a/apps/researcher/src/app/[locale]/page.tsx
+++ b/apps/researcher/src/app/[locale]/page.tsx
@@ -92,9 +92,9 @@ export default async function Home({searchParams = {}}: Props) {
let searchResult: SearchResult | undefined;
try {
searchResult = await heritageObjects.search(searchOptions);
- } catch (error) {
+ } catch (err) {
hasError = true;
- console.error(error);
+ console.error(err);
}
const locale = useLocale();
diff --git a/apps/researcher/src/app/[locale]/persons/page.tsx b/apps/researcher/src/app/[locale]/persons/page.tsx
index 82f2fc269..437d07398 100644
--- a/apps/researcher/src/app/[locale]/persons/page.tsx
+++ b/apps/researcher/src/app/[locale]/persons/page.tsx
@@ -98,9 +98,9 @@ export default async function Home({searchParams = {}}: Props) {
let searchResult: SearchResult | undefined;
try {
searchResult = await personFetcher.search(searchOptions);
- } catch (error) {
+ } catch (err) {
hasError = true;
- console.error(error);
+ console.error(err);
}
const locale = useLocale();
diff --git a/apps/researcher/src/components/add-object-list-form/actions.ts b/apps/researcher/src/components/add-object-list-form/actions.ts
new file mode 100644
index 000000000..b386b0801
--- /dev/null
+++ b/apps/researcher/src/components/add-object-list-form/actions.ts
@@ -0,0 +1,18 @@
+'use server';
+
+import {getCommunityById} from '@/lib/community';
+import {objectList} from '@colonial-collections/database';
+import {revalidatePath} from 'next/cache';
+
+interface List {
+ communityId: string;
+ createdBy: string;
+ name: string;
+ description?: string;
+}
+
+export async function addList(list: List) {
+ await objectList.create(list);
+ const community = await getCommunityById(list.communityId);
+ revalidatePath(`/[locale]/communities/${community.slug}`, 'page');
+}
diff --git a/apps/researcher/src/components/add-object-list-form/index.tsx b/apps/researcher/src/components/add-object-list-form/index.tsx
new file mode 100644
index 000000000..311ae6dd4
--- /dev/null
+++ b/apps/researcher/src/components/add-object-list-form/index.tsx
@@ -0,0 +1,151 @@
+'use client';
+
+import {useAuth} from '@clerk/nextjs';
+import {addList} from './actions';
+import {useForm, SubmitHandler} from 'react-hook-form';
+import {zodResolver} from '@hookform/resolvers/zod';
+import {insertObjectListSchema} from '@colonial-collections/database/client';
+import {useTranslations} from 'next-intl';
+import {useSlideOut, useNotifications} from 'ui';
+import {camelCase} from 'tiny-case';
+
+interface FormProps {
+ communityId: string;
+ userId: string;
+ slideOutId: string;
+}
+
+interface FormValues {
+ name: string;
+ description: string;
+ createdBy: string;
+ communityId: string;
+}
+
+function Form({communityId, userId, slideOutId}: FormProps) {
+ const {
+ register,
+ handleSubmit,
+ setError,
+ formState: {errors, isSubmitting},
+ } = useForm({
+ resolver: zodResolver(insertObjectListSchema),
+ defaultValues: {
+ name: '',
+ description: '',
+ createdBy: userId,
+ communityId,
+ },
+ });
+
+ const t = useTranslations('AddObjectListForm');
+ const {setIsVisible} = useSlideOut();
+ const {addNotification} = useNotifications();
+
+ const onSubmit: SubmitHandler = async list => {
+ try {
+ await addList(list);
+ addNotification({
+ id: 'add-object-list-success',
+ message: t.rich('listSuccessfullyAdded', {
+ name: () => {list.name},
+ }),
+ type: 'success',
+ });
+ setIsVisible(slideOutId, false);
+ } catch (err) {
+ setError('root.serverError', {
+ message: t('serverError'),
+ });
+ }
+ };
+
+ return (
+
+ );
+}
+
+interface AddObjectListFormProps {
+ communityId: string;
+ slideOutId: string;
+}
+
+export default function AddObjectListForm({
+ communityId,
+ slideOutId,
+}: AddObjectListFormProps) {
+ const {userId} = useAuth();
+ const t = useTranslations('AddObjectListForm');
+
+ // Wait for userId to be available.
+ // In most cases, this will be available immediately
+ // so no loading state is needed
+ return (
+
+
{t('title')}
+ {userId && (
+
+ )}
+
+ );
+}
diff --git a/apps/researcher/src/lib/community.test.ts b/apps/researcher/src/lib/community.test.ts
index 8528034e5..82ef5fe87 100644
--- a/apps/researcher/src/lib/community.test.ts
+++ b/apps/researcher/src/lib/community.test.ts
@@ -1,6 +1,40 @@
import {describe, expect, it} from '@jest/globals';
import {auth} from '@clerk/nextjs';
-import {isAdmin, Membership, sort, SortBy} from './community';
+import {isAdmin, sort, SortBy} from './community';
+import {OrganizationMembership} from '@clerk/nextjs/dist/types/server';
+
+const basicOrganization = {
+ id: 'organization1',
+ name: 'Organization 1',
+ slug: 'organization-1',
+ imageUrl: 'https://example.com/image.png',
+ createdAt: 1690000000000,
+ updatedAt: 1690000000000,
+ logoUrl: null,
+ createdBy: 'me',
+ publicMetadata: null,
+ privateMetadata: {},
+ maxAllowedMemberships: 100,
+ adminDeleteEnabled: true,
+};
+
+const basicMembership = {
+ id: 'membership1',
+ role: 'admin',
+ publicUserData: {
+ identifier: 'me',
+ userId: 'me',
+ firstName: 'John',
+ lastName: 'Doe',
+ profileImageUrl: 'https://example.com/image.png',
+ imageUrl: 'https://example.com/image.png',
+ },
+ publicMetadata: {},
+ privateMetadata: {},
+ createdAt: 1690000000000,
+ updatedAt: 1690000000000,
+ organization: basicOrganization,
+};
jest.mock('@clerk/nextjs', () => ({
auth: jest.fn().mockImplementation(() => ({
@@ -10,25 +44,23 @@ jest.mock('@clerk/nextjs', () => ({
describe('isAdmin', () => {
it('returns true if user is an admin', () => {
- const memberships: ReadonlyArray = [
+ const memberships: ReadonlyArray = [
{
+ ...basicMembership,
id: 'membership1',
role: 'admin',
publicUserData: {
+ ...basicMembership.publicUserData,
userId: 'me',
- firstName: 'John',
- lastName: 'Doe',
- profileImageUrl: 'https://example.com/image.png',
},
},
{
+ ...basicMembership,
id: 'membership2',
role: 'basic_member',
publicUserData: {
- userId: 'notMe',
- firstName: 'Jane',
- lastName: 'Doe',
- profileImageUrl: 'https://example.com/image.png',
+ ...basicMembership.publicUserData,
+ userId: 'me',
},
},
];
@@ -36,15 +68,14 @@ describe('isAdmin', () => {
});
it('returns false if user is not an admin', () => {
- const memberships: ReadonlyArray = [
+ const memberships: ReadonlyArray = [
{
+ ...basicMembership,
id: 'membership1',
role: 'basic_member',
publicUserData: {
+ ...basicMembership.publicUserData,
userId: 'me',
- firstName: 'John',
- lastName: 'Doe',
- profileImageUrl: 'https://example.com/image.png',
},
},
];
@@ -52,15 +83,14 @@ describe('isAdmin', () => {
});
it('returns false if user is not a member', () => {
- const memberships: ReadonlyArray = [
+ const memberships: ReadonlyArray = [
{
+ ...basicMembership,
id: 'membership1',
role: 'basic_member',
publicUserData: {
+ ...basicMembership.publicUserData,
userId: 'notMe',
- firstName: 'John',
- lastName: 'Doe',
- profileImageUrl: 'https://example.com/image.png',
},
},
];
@@ -72,23 +102,18 @@ describe('isAdmin', () => {
userId: undefined,
}));
- const memberships: ReadonlyArray = [
+ const memberships: ReadonlyArray = [
{
+ ...basicMembership,
id: 'membership1',
role: 'admin',
- publicUserData: {
- userId: 'notMe',
- firstName: 'John',
- lastName: 'Doe',
- profileImageUrl: 'https://example.com/image.png',
- },
},
];
expect(isAdmin(memberships)).toEqual(false);
});
it('returns false if there are no memberships', () => {
- const memberships: Membership[] = [];
+ const memberships: OrganizationMembership[] = [];
expect(isAdmin(memberships)).toEqual(false);
});
});
@@ -96,38 +121,33 @@ describe('isAdmin', () => {
describe('sort', () => {
const communities = [
{
+ ...basicOrganization,
id: 'community2',
name: 'Community 2',
- slug: 'community-2',
- imageUrl: 'https://example.com/image.png',
createdAt: 1690000000000,
},
{
+ ...basicOrganization,
id: 'community1',
name: 'Community 1',
- slug: 'community-1',
- imageUrl: 'https://example.com/image.png',
createdAt: 1600000000000,
},
{
+ ...basicOrganization,
id: 'community4',
name: 'Community 4',
- slug: 'community-4',
- imageUrl: 'https://example.com/image.png',
createdAt: 1680000000000,
},
{
+ ...basicOrganization,
id: 'community3',
name: 'Community 3',
- slug: 'community-3',
- imageUrl: 'https://example.com/image.png',
createdAt: 1650000000000,
},
{
+ ...basicOrganization,
id: 'community5',
name: 'Community 5',
- slug: 'community-5',
- imageUrl: 'https://example.com/image.png',
createdAt: 1670000000000,
},
];
@@ -136,137 +156,107 @@ describe('sort', () => {
const sortedCommunities = sort(communities, SortBy.NameAsc);
const expectedSortedCommunities = [
- {
+ expect.objectContaining({
id: 'community1',
name: 'Community 1',
- slug: 'community-1',
- imageUrl: 'https://example.com/image.png',
createdAt: 1600000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community2',
name: 'Community 2',
- slug: 'community-2',
- imageUrl: 'https://example.com/image.png',
createdAt: 1690000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community3',
name: 'Community 3',
- slug: 'community-3',
- imageUrl: 'https://example.com/image.png',
createdAt: 1650000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community4',
name: 'Community 4',
- slug: 'community-4',
- imageUrl: 'https://example.com/image.png',
createdAt: 1680000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community5',
name: 'Community 5',
- slug: 'community-5',
- imageUrl: 'https://example.com/image.png',
createdAt: 1670000000000,
- },
+ }),
];
- expect(sortedCommunities).toStrictEqual(expectedSortedCommunities);
+ expect(sortedCommunities).toEqual(expectedSortedCommunities);
});
it('sorts communities by name in descending order', () => {
const sortedCommunities = sort(communities, SortBy.NameDesc);
const expectedSortedCommunities = [
- {
+ expect.objectContaining({
id: 'community5',
name: 'Community 5',
- slug: 'community-5',
- imageUrl: 'https://example.com/image.png',
createdAt: 1670000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community4',
name: 'Community 4',
- slug: 'community-4',
- imageUrl: 'https://example.com/image.png',
createdAt: 1680000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community3',
name: 'Community 3',
- slug: 'community-3',
- imageUrl: 'https://example.com/image.png',
createdAt: 1650000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community2',
name: 'Community 2',
- slug: 'community-2',
- imageUrl: 'https://example.com/image.png',
createdAt: 1690000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community1',
name: 'Community 1',
- slug: 'community-1',
- imageUrl: 'https://example.com/image.png',
createdAt: 1600000000000,
- },
+ }),
];
- expect(sortedCommunities).toStrictEqual(expectedSortedCommunities);
+ expect(sortedCommunities).toEqual(expectedSortedCommunities);
});
it('sorts communities by creation date in descending order', () => {
const sortedCommunities = sort(communities, SortBy.CreatedAtDesc);
const expectedSortedCommunities = [
- {
+ expect.objectContaining({
id: 'community2',
name: 'Community 2',
- slug: 'community-2',
- imageUrl: 'https://example.com/image.png',
createdAt: 1690000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community4',
name: 'Community 4',
- slug: 'community-4',
- imageUrl: 'https://example.com/image.png',
createdAt: 1680000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community5',
name: 'Community 5',
- slug: 'community-5',
- imageUrl: 'https://example.com/image.png',
createdAt: 1670000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community3',
name: 'Community 3',
- slug: 'community-3',
- imageUrl: 'https://example.com/image.png',
createdAt: 1650000000000,
- },
- {
+ }),
+ expect.objectContaining({
id: 'community1',
name: 'Community 1',
- slug: 'community-1',
- imageUrl: 'https://example.com/image.png',
createdAt: 1600000000000,
- },
+ }),
];
- expect(sortedCommunities).toStrictEqual(expectedSortedCommunities);
+ expect(sortedCommunities).toEqual(expectedSortedCommunities);
});
it('returns the list unsorted if sortBy is an incorrect value', () => {
const sortedCommunities = sort(communities, 'incorrect' as SortBy);
- expect(sortedCommunities).toStrictEqual(communities);
+ expect(sortedCommunities).toEqual(communities);
});
});
diff --git a/apps/researcher/src/lib/community.ts b/apps/researcher/src/lib/community.ts
index a3adffb45..4e0c0162e 100644
--- a/apps/researcher/src/lib/community.ts
+++ b/apps/researcher/src/lib/community.ts
@@ -1,31 +1,21 @@
import {clerkClient, auth} from '@clerk/nextjs';
-import {
- OrganizationMembership,
- OrganizationMembershipPublicUserData,
- Organization as FullCommunity,
-} from '@clerk/backend/dist/types';
+import {OrganizationMembership, Organization} from '@clerk/backend/dist/types';
-export type Community = Pick<
- FullCommunity,
- 'id' | 'name' | 'slug' | 'imageUrl' | 'createdAt'
->;
+export type Community = Organization;
-export type Membership = Pick & {
- publicUserData?: Pick<
- OrganizationMembershipPublicUserData,
- 'userId' | 'firstName' | 'lastName' | 'profileImageUrl'
- > | null;
-};
-
-export async function getCommunityBySlug(slug: string): Promise {
+export async function getCommunityBySlug(slug: string) {
return clerkClient.organizations.getOrganization({
slug,
});
}
-export async function getMemberships(
- communityId: string
-): Promise {
+export async function getCommunityById(id: string) {
+ return clerkClient.organizations.getOrganization({
+ organizationId: id,
+ });
+}
+
+export async function getMemberships(communityId: string) {
return clerkClient.organizations.getOrganizationMembershipList({
organizationId: communityId,
});
@@ -40,7 +30,7 @@ export enum SortBy {
export const defaultSortBy = SortBy.CreatedAtDesc;
-export function sort(communities: Community[], sortBy: SortBy) {
+export function sort(communities: Organization[], sortBy: SortBy) {
// TODO: Implement sorting by membership count.
// This can be done as soon as the `Community` includes membership count.
return [...communities].sort((a, b) => {
@@ -68,7 +58,7 @@ export async function getCommunities({
sortBy = defaultSortBy,
limit = 24,
offset = 0,
-}: GetCommunitiesProps): Promise {
+}: GetCommunitiesProps) {
const communities = await clerkClient.organizations.getOrganizationList({
limit,
offset,
@@ -83,7 +73,9 @@ export async function getCommunities({
return sort(communities, sortBy);
}
-export function isAdmin(memberships: ReadonlyArray): boolean {
+export function isAdmin(
+ memberships: ReadonlyArray
+): boolean {
const {userId} = auth();
return (
diff --git a/apps/researcher/src/messages/en/messages.json b/apps/researcher/src/messages/en/messages.json
index e475309db..12e2f6062 100644
--- a/apps/researcher/src/messages/en/messages.json
+++ b/apps/researcher/src/messages/en/messages.json
@@ -143,6 +143,8 @@
"error": "Something went wrong while fetching communities. Please try again later.",
"communityName": "Community of ",
"membershipCount": "{count, plural, =0 {0 members} =1 {1 member} other {# members}}",
+ "objectListCount": "{count, plural, =0 {0 lists} =1 {1 list} other {# lists}}",
+ "objectListCountError": "Error",
"searchPlaceholder": "Search for community"
},
"Community": {
@@ -151,8 +153,23 @@
"noEntity": "Community could not be found",
"editButton": "Edit community page",
"joinButton": "Join this community",
- "objectListsTitle": "Lists of objects by community",
+ "goToListButton": "See all objects",
+ "objectListsTitle": "Lists",
+ "objectListsSubTitle": "This community has {count, plural, =0 {0 lists} =1 {1 list} other {# lists}}",
"membersTitle": "Members",
- "error": "Something went wrong while fetching the community. Please try again later."
+ "error": "Something went wrong while fetching the community. Please try again later.",
+ "addObjectListButton": "Add list"
+ },
+ "AddObjectListForm": {
+ "title": "Add list",
+ "labelName": "List name",
+ "labelDescription": "Description (optional)",
+ "labelDescriptionSubTitle": "Give a description about this list. For example, what is the history of this list. Tell a little bit about the kind of objects.",
+ "buttonSubmit": "Create list",
+ "buttonCancel": "Cancel",
+ "nameTooSmall": "List name is required",
+ "nameTooBig": "List name has a maximum of 250 characters",
+ "serverError": "Something went wrong while creating the list. Please try again later.",
+ "listSuccessfullyAdded": "List has been added to you community"
}
}
\ No newline at end of file
diff --git a/apps/researcher/src/messages/nl/messages.json b/apps/researcher/src/messages/nl/messages.json
index 5b20dd79f..974792924 100644
--- a/apps/researcher/src/messages/nl/messages.json
+++ b/apps/researcher/src/messages/nl/messages.json
@@ -142,7 +142,9 @@
"title": "Communities",
"error": "Er is een fout opgetreden bij het ophalen van de communities. Probeer het later opnieuw.",
"communityName": "Community van ",
- "membershipCount": "{count, plural, =0 {0 members} =1 {1 member} other {# members}}",
+ "membershipCount": "{count, plural, =0 {0 leden} =1 {1 lid} other {# leden}}",
+ "objectListCount": "{count, plural, =0 {0 lijsten} =1 {1 lijst} other {# lijsten}}",
+ "objectListCountError": "Error",
"searchPlaceholder": "Zoek naar community"
},
"Community": {
@@ -151,8 +153,23 @@
"noEntity": "Community kon niet gevonden worden",
"editButton": "Community page aanpassen",
"joinButton": "Word lid van deze community",
- "objectListsTitle": "Lijsten van objecten door community",
+ "goToListButton": "Bekijk alle objecten",
+ "objectListsTitle": "Lijsten",
+ "objectListsSubTitle": "Deze community heeft {count, plural, =0 {0 lijsten} =1 {1 lijst} other {# lijsten}}",
"membersTitle": "Leden",
- "error": "Er is een fout opgetreden bij het ophalen van de community. Probeer het later opnieuw."
+ "error": "Er is een fout opgetreden bij het ophalen van de community. Probeer het later opnieuw.",
+ "addObjectListButton": "Lijst toevoegen"
+ },
+ "AddObjectListForm": {
+ "title": "Lijst toevoegen",
+ "labelName": "Lijstnaam",
+ "labelDescription": "Beschrijving (optioneel)",
+ "labelDescriptionSubTitle": "Geef een beschrijving over deze lijst. Bijvoorbeeld, wat is de geschiedenis van deze lijst. Vertel iets over het soort objecten.",
+ "buttonSubmit": "Lijst aanmaken",
+ "buttonCancel": "Annuleren",
+ "nameTooSmall": "Lijstnaam is vereist",
+ "nameTooBig": "Lijstnaam mag maximaal 250 tekens bevatten",
+ "serverError": "Er is iets misgegaan bij het maken van de lijst. Probeer het later opnieuw.",
+ "listSuccessfullyAdded": "Lijst is aan je community toegevoegd"
}
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index f2f9927a0..ac5fa225d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -66,6 +66,12 @@
"node": "18.x"
}
},
+ "apps/dataset-browser/node_modules/@next/env": {
+ "version": "13.5.3",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.3.tgz",
+ "integrity": "sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==",
+ "dev": true
+ },
"apps/dataset-browser/node_modules/zod": {
"version": "3.22.2",
"license": "MIT",
@@ -87,6 +93,7 @@
"@hapi/hoek": "11.0.2",
"@headlessui/react": "1.7.17",
"@heroicons/react": "2.0.18",
+ "@hookform/resolvers": "3.3.1",
"@next/mdx": "13.5.3",
"classnames": "2.3.2",
"fetch-sparql-endpoint": "3.3.3",
@@ -96,6 +103,8 @@
"rdf-object": "1.14.0",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "7.46.1",
+ "tiny-case": "1.0.3",
"zod": "3.22.2",
"zustand": "4.3.9"
},
@@ -129,9 +138,16 @@
"node": "18.x"
}
},
+ "apps/researcher/node_modules/@next/env": {
+ "version": "13.5.3",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.3.tgz",
+ "integrity": "sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==",
+ "dev": true
+ },
"apps/researcher/node_modules/zod": {
"version": "3.22.2",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz",
+ "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -1381,6 +1397,14 @@
"react": ">= 16"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.1.tgz",
+ "integrity": "sha512-K7KCKRKjymxIB90nHDQ7b9nli474ru99ZbqxiqDAWYsYhOsU3/4qLxW91y+1n04ic13ajjZ66L3aXbNef8PELQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"dev": true,
@@ -1774,10 +1798,9 @@
"license": "MIT"
},
"node_modules/@next/env": {
- "version": "13.5.3",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.3.tgz",
- "integrity": "sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==",
- "dev": true
+ "version": "13.5.2",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.2.tgz",
+ "integrity": "sha512-dUseBIQVax+XtdJPzhwww4GetTjlkRSsXeQnisIJWBaHsnxYcN2RGzsPHi58D6qnkATjnhuAtQTJmR1hKYQQPg=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.5.3",
@@ -1829,9 +1852,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.2.tgz",
- "integrity": "sha512-7eAyunAWq6yFwdSQliWMmGhObPpHTesiKxMw4DWVxhm5yLotBj8FCR4PXGkpRP2tf8QhaWuVba+/fyAYggqfQg==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz",
+ "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==",
"cpu": [
"arm64"
],
@@ -1844,9 +1867,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.2.tgz",
- "integrity": "sha512-WxXYWE7zF1ch8rrNh5xbIWzhMVas6Vbw+9BCSyZvu7gZC5EEiyZNJsafsC89qlaSA7BnmsDXVWQmc+s1feSYbQ==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz",
+ "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==",
"cpu": [
"x64"
],
@@ -1859,9 +1882,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.2.tgz",
- "integrity": "sha512-URSwhRYrbj/4MSBjLlefPTK3/tvg95TTm6mRaiZWBB6Za3hpHKi8vSdnCMw5D2aP6k0sQQIEG6Pzcfwm+C5vrg==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz",
+ "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==",
"cpu": [
"arm64"
],
@@ -1874,9 +1897,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.2.tgz",
- "integrity": "sha512-HefiwAdIygFyNmyVsQeiJp+j8vPKpIRYDlmTlF9/tLdcd3qEL/UEBswa1M7cvO8nHcr27ZTKXz5m7dkd56/Esg==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz",
+ "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==",
"cpu": [
"arm64"
],
@@ -1919,9 +1942,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.2.tgz",
- "integrity": "sha512-Em9ApaSFIQnWXRT3K6iFnr9uBXymixLc65Xw4eNt7glgH0eiXpg+QhjmgI2BFyc7k4ZIjglfukt9saNpEyolWA==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz",
+ "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==",
"cpu": [
"arm64"
],
@@ -1934,9 +1957,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.2.tgz",
- "integrity": "sha512-TBACBvvNYU+87X0yklSuAseqdpua8m/P79P0SG1fWUvWDDA14jASIg7kr86AuY5qix47nZLEJ5WWS0L20jAUNw==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz",
+ "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==",
"cpu": [
"ia32"
],
@@ -1949,9 +1972,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.2.tgz",
- "integrity": "sha512-LfTHt+hTL8w7F9hnB3H4nRasCzLD/fP+h4/GUVBTxrkMJOnh/7OZ0XbYDKO/uuWwryJS9kZjhxcruBiYwc5UDw==",
+ "version": "13.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz",
+ "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==",
"cpu": [
"x64"
],
@@ -2410,9 +2433,8 @@
},
"node_modules/@types/n3": {
"version": "1.16.0",
- "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.0.tgz",
- "integrity": "sha512-g/67NVSihmIoIZT3/J462NhJrmpCw+5WUkkKqpCE9YxNEWzBwKavGPP+RUmG6DIm5GrW4GPunuxLJ0Yn/GgNjQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@rdfjs/types": "^1.1.0",
"@types/node": "*"
@@ -3551,9 +3573,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001539",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001539.tgz",
- "integrity": "sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==",
+ "version": "1.0.30001543",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz",
+ "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==",
"funding": [
{
"type": "opencollective",
@@ -9213,11 +9235,6 @@
"version": "1.1.0",
"license": "ISC"
},
- "node_modules/next/node_modules/@next/env": {
- "version": "13.5.2",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.2.tgz",
- "integrity": "sha512-dUseBIQVax+XtdJPzhwww4GetTjlkRSsXeQnisIJWBaHsnxYcN2RGzsPHi58D6qnkATjnhuAtQTJmR1hKYQQPg=="
- },
"node_modules/next/node_modules/postcss": {
"version": "8.4.14",
"funding": [
@@ -10106,6 +10123,21 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.46.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.46.1.tgz",
+ "integrity": "sha512-0GfI31LRTBd5tqbXMGXT1Rdsv3rnvy0FjEk8Gn9/4tp6+s77T7DPZuGEpBRXOauL+NhyGT5iaXzdIM2R6F/E+w==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"license": "MIT"
@@ -11262,6 +11294,11 @@
"next-tick": "1"
}
},
+ "node_modules/tiny-case": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
+ },
"node_modules/tiny-lru": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.0.tgz",
@@ -12337,7 +12374,8 @@
"classnames": "2.3.2",
"next": "13.5.2",
"next-intl": "3.0.0-beta.7",
- "react": "18.2.0"
+ "react": "18.2.0",
+ "zustand": "4.4.1"
},
"devDependencies": {
"@types/react": "18.2.20",
@@ -12348,6 +12386,33 @@
"tsconfig": "*",
"typescript": "5.1.6"
}
+ },
+ "packages/ui/node_modules/zustand": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
+ "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/packages/database/client.ts b/packages/database/client.ts
new file mode 100644
index 000000000..ff5d1464c
--- /dev/null
+++ b/packages/database/client.ts
@@ -0,0 +1 @@
+export * from './src/db/validation';
diff --git a/packages/database/index.ts b/packages/database/index.ts
new file mode 100644
index 000000000..506cb41dc
--- /dev/null
+++ b/packages/database/index.ts
@@ -0,0 +1 @@
+export * as objectList from './src/object-list';
diff --git a/packages/database/migrations/0002_object_list_name_not_null.sql b/packages/database/migrations/0002_object_list_name_not_null.sql
new file mode 100644
index 000000000..94ece24e4
--- /dev/null
+++ b/packages/database/migrations/0002_object_list_name_not_null.sql
@@ -0,0 +1 @@
+ALTER TABLE `object_list` MODIFY COLUMN `name` varchar(256) NOT NULL;
\ No newline at end of file
diff --git a/packages/database/migrations/meta/0002_snapshot.json b/packages/database/migrations/meta/0002_snapshot.json
new file mode 100644
index 000000000..6cb787aed
--- /dev/null
+++ b/packages/database/migrations/meta/0002_snapshot.json
@@ -0,0 +1,165 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "f2311843-75be-4ca1-be64-e5620a7b268b",
+ "prevId": "cf9627d1-a3e1-4768-8539-90a87f3e0a97",
+ "tables": {
+ "object_item": {
+ "name": "object_item",
+ "columns": {
+ "object_id": {
+ "name": "object_id",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "object_iri": {
+ "name": "object_iri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "object_list_id": {
+ "name": "object_list_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "id": {
+ "name": "id",
+ "columns": [
+ "object_id"
+ ],
+ "isUnique": false
+ },
+ "object_list_id": {
+ "name": "object_list_id",
+ "columns": [
+ "object_list_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "object_item_object_id": {
+ "name": "object_item_object_id",
+ "columns": [
+ "object_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "object_item_object_id_object_list_id_unique": {
+ "name": "object_item_object_id_object_list_id_unique",
+ "columns": [
+ "object_id",
+ "object_list_id"
+ ]
+ }
+ }
+ },
+ "object_list": {
+ "name": "object_list",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "community_id": {
+ "name": "community_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "community_id": {
+ "name": "community_id",
+ "columns": [
+ "community_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "object_list_id": {
+ "name": "object_list_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "schemas": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json
index f983b0fa7..58d559d8d 100644
--- a/packages/database/migrations/meta/_journal.json
+++ b/packages/database/migrations/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1695286872562,
"tag": "0001_create_object_item",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "5",
+ "when": 1695393273717,
+ "tag": "0002_object_list_name_not_null",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/database/package.json b/packages/database/package.json
index 6ad6c9a49..fe9c4b497 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -2,8 +2,8 @@
"name": "@colonial-collections/database",
"version": "0.0.0",
"private": true,
- "main": "./src/index.ts",
- "types": "./src/index.ts",
+ "main": "./index.ts",
+ "types": "./index.ts",
"scripts": {
"db:generate": "dotenv -c -- drizzle-kit generate:mysql",
"db:migrate": "dotenv -c -- ts-node ./scripts/migrate.ts",
diff --git a/packages/database/src/db/schema.ts b/packages/database/src/db/schema.ts
index 9c6f849a1..d0f858dd4 100644
--- a/packages/database/src/db/schema.ts
+++ b/packages/database/src/db/schema.ts
@@ -9,13 +9,12 @@ import {
int,
} from 'drizzle-orm/mysql-core';
import {sql, relations} from 'drizzle-orm';
-import {createInsertSchema} from 'drizzle-zod';
export const objectLists = mysqlTable(
'object_list',
{
id: serial('id').primaryKey(),
- name: varchar('name', {length: 256}),
+ name: varchar('name', {length: 256}).notNull(),
description: text('description'),
communityId: varchar('community_id', {length: 50}),
createdAt: timestamp('created_at')
@@ -63,5 +62,3 @@ export const objectItemsRelations = relations(objectItems, ({one}) => ({
references: [objectLists.id],
}),
}));
-
-export const insertObjectItemSchema = createInsertSchema(objectItems);
diff --git a/packages/database/src/db/validation.ts b/packages/database/src/db/validation.ts
new file mode 100644
index 000000000..f0bd84420
--- /dev/null
+++ b/packages/database/src/db/validation.ts
@@ -0,0 +1,8 @@
+import {createInsertSchema} from 'drizzle-zod';
+import {objectItems, objectLists} from './schema';
+
+export const insertObjectListSchema = createInsertSchema(objectLists, {
+ name: schema => schema.name.trim().min(1),
+});
+
+export const insertObjectItemSchema = createInsertSchema(objectItems);
diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts
deleted file mode 100644
index beec8dd62..000000000
--- a/packages/database/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './object-list';
diff --git a/packages/database/src/object-list.ts b/packages/database/src/object-list.ts
index 37b1ffb34..bc6639312 100644
--- a/packages/database/src/object-list.ts
+++ b/packages/database/src/object-list.ts
@@ -1,56 +1,77 @@
-'use server';
-
-import {objectLists, insertObjectItemSchema, objectItems} from './db/schema';
+import {objectLists, objectItems} from './db/schema';
+import {insertObjectItemSchema, insertObjectListSchema} from './db/validation';
+import {InferSelectModel, sql} from 'drizzle-orm';
import {db} from './db/connection';
import {iriToHash} from './iri-to-hash';
+import {DBQueryConfig, eq} from 'drizzle-orm';
-async function getListsByCommunityId(communityId: string) {
- return db.query.objectLists.findMany({
- where: (objectLists, {eq}) => eq(objectLists.communityId, communityId),
- });
+interface Option {
+ withObjects?: boolean;
+ limitObjects?: number;
}
-async function getCommunityListsWithObjects(communityId: string) {
+interface ObjectList extends InferSelectModel {
+ objects?: InferSelectModel[];
+}
+
+export async function getByCommunityId(
+ communityId: string,
+ {withObjects, limitObjects}: Option = {withObjects: false}
+): Promise {
+ const options: DBQueryConfig = {};
+
+ if (withObjects) {
+ options.with = {
+ objects: limitObjects ? {limit: limitObjects} : true,
+ };
+ }
+
return db.query.objectLists.findMany({
+ ...options,
where: (objectLists, {eq}) => eq(objectLists.communityId, communityId),
- with: {
- objects: true,
- },
});
}
-interface CreateListForCommunityProps {
+export async function countByCommunityId(communityId: string) {
+ const result = await db
+ .select({count: sql`count(*)`})
+ .from(objectLists)
+ .where(eq(objectLists.communityId, communityId));
+
+ // We assume that the aggregations with `count` always returns an array with one value that is an object with the count prop
+ return result[0].count;
+}
+
+interface CreateProps {
communityId: string;
- userId: string;
name: string;
- description: string;
+ createdBy: string;
+ description?: string;
}
-async function createListForCommunity({
+export async function create({
communityId,
name,
+ createdBy,
description,
- userId,
-}: CreateListForCommunityProps) {
- return db.insert(objectLists).values({
+}: CreateProps) {
+ const objectList = insertObjectListSchema.parse({
communityId,
name,
description,
- createdBy: userId,
+ createdBy,
});
+
+ return db.insert(objectLists).values(objectList);
}
-interface AddObjectToListProps {
+interface AddObjectProps {
listId: number;
objectIri: string;
userId: string;
}
-async function addObjectToList({
- listId,
- objectIri,
- userId,
-}: AddObjectToListProps) {
+export async function addObject({listId, objectIri, userId}: AddObjectProps) {
const objectItem = insertObjectItemSchema.parse({
listId,
objectIri,
@@ -60,10 +81,3 @@ async function addObjectToList({
return db.insert(objectItems).values(objectItem);
}
-
-export const objectList = {
- getListsByCommunityId,
- getCommunityListsWithObjects,
- createListForCommunity,
- addObjectToList,
-};
diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json
index 3200e3f01..cf42845ea 100644
--- a/packages/database/tsconfig.json
+++ b/packages/database/tsconfig.json
@@ -4,5 +4,5 @@
"rootDir": "./src",
"outDir": "./build"
},
- "include": ["src/**/*.ts"]
+ "include": ["./**/*.ts"]
}
diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx
index bba021673..f2409a51c 100644
--- a/packages/ui/index.tsx
+++ b/packages/ui/index.tsx
@@ -4,3 +4,5 @@ export * from './localized-markdown';
export * from './small-screen-sub-menu';
export * from './wip-message';
export * from './slide-over';
+export * from './slide-out';
+export * from './notifications';
diff --git a/packages/ui/notifications.tsx b/packages/ui/notifications.tsx
new file mode 100644
index 000000000..dcf00e0dc
--- /dev/null
+++ b/packages/ui/notifications.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import {create} from 'zustand';
+import {XMarkIcon} from '@heroicons/react/24/outline';
+import {ReactNode, useEffect} from 'react';
+import {usePathname} from 'next/navigation';
+
+type Notification = {
+ id: string;
+ message: ReactNode;
+ type: 'success'; // For now we only have success notifications, but we could add more types later
+};
+
+interface State {
+ notifications: Notification[];
+ addNotification(item: Notification): void;
+ removeNotification(item: Notification): void;
+ reset(): void;
+}
+
+export const useNotifications = create(set => ({
+ notifications: [],
+ addNotification: notification =>
+ set(state => ({
+ notifications: [...state.notifications, notification],
+ })),
+ removeNotification: notification =>
+ set(state => ({
+ notifications: state.notifications.filter(i => i.id !== notification.id),
+ })),
+ reset: () =>
+ set(() => ({
+ notifications: [],
+ })),
+}));
+
+export function Notifications() {
+ const {notifications, removeNotification, reset} = useNotifications();
+ const pathname = usePathname();
+
+ useEffect(() => {
+ // Reset notifications when the route changes
+ reset();
+ }, [pathname, reset]);
+
+ if (notifications.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {notifications.map(notification => (
+
+
{notification.message}
+
+
+ ))}
+
+ );
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 3b1163cd6..1c2aa1442 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -21,6 +21,7 @@
"classnames": "2.3.2",
"next": "13.5.2",
"next-intl": "3.0.0-beta.7",
- "react": "18.2.0"
+ "react": "18.2.0",
+ "zustand": "4.4.1"
}
}
diff --git a/packages/ui/slide-out.tsx b/packages/ui/slide-out.tsx
new file mode 100644
index 000000000..04d749f50
--- /dev/null
+++ b/packages/ui/slide-out.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import {create} from 'zustand';
+import {ReactNode, ButtonHTMLAttributes, useEffect} from 'react';
+import {usePathname} from 'next/navigation';
+
+interface SlideOutState {
+ visibleIds: {
+ [id: string]: Boolean;
+ };
+ setIsVisible: (id: string, isVisible: Boolean) => void;
+ isVisible: (id: string) => Boolean;
+}
+
+export const useSlideOut = create((set, get) => ({
+ visibleIds: {},
+ setIsVisible: (id, isVisible) =>
+ set(state => ({
+ visibleIds: {
+ ...state.visibleIds,
+ [id]: isVisible,
+ },
+ })),
+ isVisible: id => {
+ const visible = get().visibleIds[id];
+ return visible === undefined ? false : visible;
+ },
+}));
+
+interface SlideOutButtonProps {
+ id: string;
+ children: ReactNode;
+}
+
+export function SlideOutButton({
+ id,
+ children,
+ ...buttonProps
+}: SlideOutButtonProps & ButtonHTMLAttributes) {
+ const {setIsVisible, isVisible} = useSlideOut();
+ return (
+
+ );
+}
+
+interface SlideOutProps {
+ id: string;
+ children: ReactNode;
+}
+
+export function SlideOut({id, children}: SlideOutProps) {
+ const {isVisible, setIsVisible} = useSlideOut();
+ const pathname = usePathname();
+
+ useEffect(() => {
+ // Close slide out when the route changes
+ setIsVisible(id, false);
+ }, [id, pathname, setIsVisible]);
+
+ return isVisible(id) ? <>{children}> : null;
+}