Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Community description #258

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/researcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@
"@clerk/nextjs": "4.23.2",
"@clerk/types": "3.51.0",
"@colonial-collections/content": "*",
"@colonial-collections/database": "*",
"@colonial-collections/iris": "*",
"@colonial-collections/label-fetcher": "*",
"@colonial-collections/list-store": "*",
"@hapi/hoek": "11.0.2",
"@headlessui/react": "1.7.16",
"@heroicons/react": "2.0.18",
"@hookform/resolvers": "3.3.1",
"@next/mdx": "13.4.19",
"classnames": "2.3.2",
"fetch-sparql-endpoint": "3.3.3",
"next": "13.5.2",
"next-intl": "3.0.0-beta.7",
"@colonial-collections/database": "*",
"openseadragon": "4.1.0",
"rdf-object": "1.14.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.46.1",
"zod": "3.22.2",
"zustand": "4.3.9"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
<h1>This page is under construction</h1>
</div>
);
}
27 changes: 25 additions & 2 deletions apps/researcher/src/app/[locale]/communities/[slug]/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
'use server';

import {joinCommunity} from '@/lib/community';
import {joinCommunity, editDescription} from '@/lib/community';
import {revalidatePath} from 'next/cache';

interface editDescriptionActionProps {
communityId: string;
communitySlug: string;
description: string;
}

async function editDescriptionAction({
communityId,
communitySlug,
description,
}: editDescriptionActionProps) {
try {
await editDescription({communityId, description});
revalidatePath(`/[locale]/communities/${communitySlug}`, 'page');
revalidatePath('/[locale]/communities', 'page');

return {statusCode: 200};
} catch (err) {
return {statusCode: 500};
}
}

// Export as server actions.
export {joinCommunity};
export {joinCommunity, editDescriptionAction};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client';

import {useForm, SubmitHandler} from 'react-hook-form';
import {useTranslations} from 'next-intl';
import {useSlideOut, useNotifications} from 'ui';
import {editDescriptionAction} from './actions';

interface Props {
communityId: string;
communitySlug: string;
slideOutId: string;
description?: string;
}

interface FormValues {
description: string;
communityId: string;
communitySlug: string;
}

export default function EditDescriptionForm({
slideOutId,
communityId,
communitySlug,
description,
}: Props) {
const {
register,
handleSubmit,
setError,
formState: {errors, isSubmitting},
} = useForm({
defaultValues: {
description: description ?? '',
communityId,
communitySlug,
},
});

const t = useTranslations('Community');
const {setIsVisible} = useSlideOut();
const {addNotification} = useNotifications();

const onSubmit: SubmitHandler<FormValues> = async data => {
const response = await editDescriptionAction(data);

if (response.statusCode > 200) {
setError('root.serverError', {
message: t('serverError'),
});
} else {
addNotification({
id: 'add-object-list-success',
message: <>{t('descriptionSuccessfullyEdited')}</>,
type: 'success',
});
setIsVisible(slideOutId, false);
}
};

return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex-col gap-6 flex w-full max-w-3xl"
>
{errors.root?.serverError.message && (
<div className="rounded-md bg-red-50 p-4 mt-3">
<div className="ml-3">
<h3 className="text-sm leading-5 font-medium text-red-800">
{errors.root.serverError.message}
</h3>
</div>
</div>
)}
<div className="flex flex-col sm:flex-row gap-4 ">
<div className="flex flex-col w-full">
<label className="flex flex-col text-sm">
<strong>{t('labelDescription')}</strong>
</label>
<textarea
id="description"
{...register('description', {
maxLength: 2000,
})}
rows={4}
className="border border-greenGrey-200 rounded p-2 w-full"
/>
<p>
{errors.description && t(`description_${errors.description.type}`)}
</p>
</div>
</div>

<div className="flex flex-col lg:flex-row gap-4">
<div className="w-full lg:w-2/3 flex gap-2">
<button
disabled={isSubmitting}
type="submit"
className="p-1 sm:py-2 sm:px-3 rounded-full text-xs bg-greenGrey-100 hover:bg-greenGrey-200 transition text-greenGrey-800 flex items-center gap-1"
>
{t('changeDescriptionSaveButton')}
</button>
<button
onClick={() => setIsVisible(slideOutId, false)}
className="p-1 sm:py-2 sm:px-3 rounded-full text-xs border border-greenGrey-300 hover:bg-greenGrey-200 transition text-greenGrey-800 flex items-center gap-1"
>
{t('changeDescriptionCancelButton')}
</button>
</div>
</div>
</form>
);
}
19 changes: 19 additions & 0 deletions apps/researcher/src/app/[locale]/communities/[slug]/object.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="border border-blueGrey-200 text-blueGrey-800 bg-greenGrey-50 rounded-sm p-2 text-xs">
{object.name}
</div>
);
}
138 changes: 121 additions & 17 deletions apps/researcher/src/app/[locale]/communities/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ 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, SlideOutClosed, Notifications} from 'ui';
import EditDescriptionForm from './edit-description-form';

interface Props {
params: {
Expand All @@ -15,6 +20,13 @@ interface Props {
};
}

const slideOutFormId = 'add-object-list';
const slideOutDescriptionId = 'edit-community-description';

// Don't cache this page, so we always get the latest community data from the third-party Clerk.
// With 'force-no-store', the description will change after editing.
export const fetchCache = 'force-no-store';

export default async function CommunityPage({params}: Props) {
const t = await getTranslator(params.locale, 'Community');

Expand All @@ -39,9 +51,19 @@ export default async function CommunityPage({params}: Props) {
return <ErrorMessage error={t('error')} />;
}

let objectLists;
try {
objectLists = await objectList.getCommunityListsWithObjects({
communityId: community.id,
limitObjects: 4,
});
} catch (err) {
return <ErrorMessage error={t('error')} />;
}

return (
<>
<div className=" px-4 sm:px-10 -mt-3 -mb-3 sm:-mb-9 flex gap-2 flex-row sm:justify-between w-full max-w-[1800px] mx-auto">
<div className="px-4 sm:px-10 -mt-3 -mb-3 sm:-mb-9 flex gap-2 flex-row sm:justify-between w-full max-w-[1800px] mx-auto">
<div>
<Link href="/communities" className="flex items-center gap-1">
<ChevronLeftIcon className="w-4 h-4 fill-neutral-500" />
Expand All @@ -52,27 +74,109 @@ export default async function CommunityPage({params}: Props) {
{isAdmin(memberships) && <EditCommunityButton />}
</div>
</div>
<div className="px-4 my-10 sm:px-10 w-full max-w-[1800px] mx-auto">
<h1 className="text-2xl md:text-4xl font-normal">
{t('title')}
<span className="font-semibold ml-2" data-testid="community-name">
{community.name}
</span>
</h1>
</div>
<div className="flex flex-col md:flex-row h-full items-stretch grow content-stretch self-stretch gap-4 md:gap-16 w-full max-w-[1800px] mx-auto px-4 sm:px-10">
<main className="w-full md:w-3/4">
<div className="w-full flex flex-col md:flex-row justify-between">
<div className="mb-4 max-w-3xl">
{/*Place the description here*/}
<div className="flex flex-col md:flex-row h-full items-stretch grow content-stretch self-stretch gap-4 md:gap-16 w-full max-w-[1800px] mx-auto px-4 sm:px-10 mt-12">
<main className="w-full">
<div className="-mb-16 md:-mb-24 w-full flex justify-center">
<div className="w-32 h-32 lg:w-48 lg:h-48 rounded-full overflow-hidden relative">
<Image
fill
sizes="(min-width: 1024px) 192px, 128px"
src={community.imageUrl}
alt=""
/>
</div>
</div>
<div className="w-full rounded-lg bg-[#f3eee2] text-stone-800 pt-16 md:pt-24 pb-6">
<h1 className="text-2xl font-normal w-full text-center mt-4 px-4 my-6">
{t('title')}
<span className="font-semibold ml-2" data-testid="community-name">
{community.name}
</span>
</h1>
<div className="w-full flex flex-col md:flex-row justify-center px-4">
<SlideOutClosed id={slideOutDescriptionId}>
<div className="mb-4 max-w-3xl text-left whitespace-pre-line">
{community.publicMetadata?.description}
</div>
</SlideOutClosed>
<SlideOut id={slideOutDescriptionId}>
<EditDescriptionForm
slideOutId={slideOutDescriptionId}
communityId={community.id}
communitySlug={community.slug!}
description={community.publicMetadata?.description}
/>
</SlideOut>
</div>
<div className="flex flex-col items-start md:justify-center md:items-center w-full mb-4">
<JoinCommunityButton communityId={community.id} />
<SlideOutButton
hideIfOpen
id={slideOutDescriptionId}
className="flex items-center py-2 px-3 rounded-full bg-sand-100 text-sand-900 hover:bg-white transition text-xs"
>
{t('editDescriptionButton')}
</SlideOutButton>
</div>
</div>
<h2 className="mb-6">{t('objectListsTitle')}</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 w-full">
{/*Place the lists here */}

<div className="mt-12">
<div className="flex justify-between my-4">
<div>
<h2>{t('objectListsTitle')}</h2>
<p>{t('objectListsSubTitle', {count: objectLists.length})}</p>
</div>
<div>
{isAdmin(memberships) && (
<SlideOutButton
id={slideOutFormId}
className="flex items-center py-2 px-3 rounded-full bg-sand-100 text-sand-900 hover:bg-white transition text-xs"
>
{t('addObjectListButton')}
</SlideOutButton>
)}
</div>
</div>

<Notifications />
<SlideOut id={slideOutFormId}>
<AddObjectListForm
slideOutId={slideOutFormId}
communityId={community.id}
/>
</SlideOut>

<div className="grid grid-cols-1 xl:grid-cols-2 gap-6 xl:gap-16">
{objectLists.map(objectList => (
<Link
href={`/communities/${params.slug}/${objectList.id}`}
key={objectList.id}
className="text-neutral-800"
>
<h3 className="font-semibold text-xl mt-4 mb-2">
{objectList.name}
</h3>
<p>{objectList.description}</p>

<div className="w-full relative">
<ul className=" mt-4 grid grid-cols-4 gap-2">
{objectList.objects.map(object => (
<ObjectCard
key={object.objectId}
objectIri={object.objectIri}
/>
))}
</ul>

<div className="absolute bg-gradient-to-l from-white w-full top-0 bottom-0 flex justify-end">
<button className="p-2 self-center flex items-center py-2 px-3 rounded-full bg-sand-100 text-sand-900 hover:bg-white transition text-xs">
{t('goToListButton')}
</button>
</div>
</div>
</Link>
))}
</div>
</div>
</main>
<aside className="w-full md:w-1/4 self-stretch">
Expand Down
Loading