Skip to content

Commit

Permalink
Merge pull request #315 from colonial-heritage/edit-community
Browse files Browse the repository at this point in the history
Custom edit community form with ORCID iD
  • Loading branch information
barbarah authored Nov 9, 2023
2 parents 19329e4 + 7d9e088 commit fbdad82
Show file tree
Hide file tree
Showing 15 changed files with 486 additions and 233 deletions.
2 changes: 1 addition & 1 deletion apps/dataset-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"tiny-lru": "11.2.0",
"zod": "3.22.2",
"zod": "3.21.4",
"zustand": "4.3.9"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions apps/researcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@hapi/hoek": "11.0.2",
"@headlessui/react": "1.7.17",
"@heroicons/react": "2.0.18",
"@hookform/resolvers": "3.3.1",
"@hookform/resolvers": "3.3.2",
"@next/mdx": "14.0.1",
"classnames": "2.3.2",
"fetch-sparql-endpoint": "4.1.0",
Expand All @@ -43,9 +43,9 @@
"rdf-object": "1.14.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.46.1",
"react-hook-form": "7.48.2",
"tiny-case": "1.0.3",
"zod": "3.22.2",
"zod": "3.21.4",
"zustand": "4.3.9"
},
"devDependencies": {
Expand Down
33 changes: 23 additions & 10 deletions apps/researcher/src/app/[locale]/communities/[slug]/actions.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
'use server';

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

interface UpdateDescriptionAndRevalidateProps {
communityId: string;
communitySlug: string;
interface UpdateCommunityAndRevalidateProps {
id: string;
name: string;
slug: string;
description: string;
attributionId: string;
}

export async function updateDescriptionAndRevalidate({
communityId,
communitySlug,
export async function updateCommunityAndRevalidate({
id,
name,
slug,
description,
}: UpdateDescriptionAndRevalidateProps) {
await updateDescription({communityId, description});
revalidatePath(`/[locale]/communities/${communitySlug}`, 'page');
attributionId,
}: UpdateCommunityAndRevalidateProps) {
const community = await updateCommunity({
id,
description,
slug,
name,
attributionId,
});

revalidatePath(`/[locale]/communities/${slug}`, 'page');
revalidatePath('/[locale]/communities', 'page');

return community;
}

interface JoinCommunityAndRevalidateProps {
Expand Down
92 changes: 82 additions & 10 deletions apps/researcher/src/app/[locale]/communities/[slug]/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import {useState} from 'react';
import {useTranslations} from 'next-intl';
import {useClerk, useUser} from '@clerk/nextjs';
import {useClerk, useUser, useOrganizationList} from '@clerk/nextjs';
import {useTransition} from 'react';
import {joinCommunityAndRevalidate} from './actions';
import {PencilSquareIcon, ChevronDownIcon} from '@heroicons/react/24/solid';
import {Fragment} from 'react';
import {Menu, Transition} from '@headlessui/react';
import {SlideOutButton} from '@colonial-collections/ui';

interface Props {
communityId: string;
Expand Down Expand Up @@ -73,18 +77,86 @@ export function JoinCommunityButton({communityId, communitySlug}: Props) {
);
}

export function EditCommunityButton() {
const menuItems = [
{translationKey: 'settingsButton', page: 'settings'},
{translationKey: 'membersButton', page: 'members'},
];

interface ButtonGroupProps {
slideOutEditFormId: string;
communityId: string;
communitySlug: string;
}

export function ButtonGroup({
slideOutEditFormId,
communityId,
communitySlug,
}: ButtonGroupProps) {
const t = useTranslations('Community');
const {openOrganizationProfile} = useClerk();
const {setActive} = useOrganizationList();

const openClerkProfile = (firstPage: string) => {
// Only the active organization can be edited.
setActive && setActive({organization: communityId});
// We want to show only one page of the organization's profile.
// But it is impossible to load only one page, so the next best thing is to hide the navbar and only show the first page.
// We must place all pages in `customPage` to define the page order.
// Pages not in `customPages` will load before the custom pages. So, we need to add all pages to control the first loaded page.
openOrganizationProfile({
afterLeaveOrganizationUrl: `/revalidate/?path=/[locale]/communities/${communitySlug}&redirect=/communities/${communitySlug}`,
customPages: ['settings', 'members']
.sort((a, b) => {
return a === firstPage ? -1 : b === firstPage ? 1 : 0;
})
.map(page => ({label: page})),
appearance: {
elements: {
navbar: 'hidden',
},
},
});
};

return (
<button
onClick={() => {
openOrganizationProfile();
}}
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('editButton')}
</button>
<div className="inline-flex rounded-md shadow-sm">
<SlideOutButton
id={slideOutEditFormId}
className="relative inline-flex items-center rounded-l-md bg-neutral-200 hover:bg-neutral-300 px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10"
>
<PencilSquareIcon className="w-5 h-5 fill-neutral-700" />
{t('editButton')}
</SlideOutButton>
<Menu as="div" className="relative -ml-px block">
<Menu.Button className="relative inline-flex items-center rounded-r-md bg-neutral-200 hover:bg-neutral-300 px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 focus:z-10">
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 -mr-1 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{menuItems.map(item => (
<Menu.Item key={item.translationKey}>
<button
className="block px-4 py-2 text-sm text-gray-700"
onClick={() => openClerkProfile(item.page)}
>
{t(item.translationKey)}
</button>
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use client';

import {useForm, SubmitHandler} from 'react-hook-form';
import {useTranslations} from 'next-intl';
import {useSlideOut, useNotifications} from '@colonial-collections/ui';
import {z} from 'zod';
import {zodResolver} from '@hookform/resolvers/zod';
import {updateCommunityAndRevalidate} from './actions';
import {camelCase} from 'tiny-case';
import {useRouter} from 'next-intl/client';

interface Props {
communityId: string;
slideOutId: string;
name: string;
slug: string;
description?: string;
attributionId?: string;
}

interface FormValues {
name: string;
slug: string;
description: string;
attributionId: string;
}

const communitySchema = z.object({
name: z.string().trim().min(1).max(250),
slug: z
.string()
.trim()
.min(1)
.max(250)
.regex(/^[a-z0-9-]*$/),
description: z.string().max(2000),
attributionId: z.string().url().optional().or(z.literal('')),
});

export default function EditCommunityForm({
slideOutId,
communityId,
name,
slug,
description,
attributionId,
}: Props) {
const {
register,
handleSubmit,
setError,
formState: {errors, isSubmitting},
} = useForm({
resolver: zodResolver(communitySchema),
defaultValues: {
name,
slug,
description: description ?? '',
attributionId: attributionId ?? '',
},
});

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

const onSubmit: SubmitHandler<FormValues> = async formValues => {
try {
const newCommunity = await updateCommunityAndRevalidate({
id: communityId,
...formValues,
});
router.push(`/communities/${newCommunity.slug}`, {scroll: false});
addNotification({
id: 'add-object-list-success',
message: <>{t('communityUpdated')}</>,
type: 'success',
});
setIsVisible(slideOutId, false);
} catch (err) {
setError('root.serverError', {
message: t('communityEditServerError'),
});
}
};

return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex-col px-4 gap-4 items-center flex"
>
<h1 className="text-2xl font-normal w-full text-center mt-4 px-4 my-2">
{t('editCommunityTitle')}
</h1>
{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 max-w-2xl w-full">
<label className="italic">{t('labelName')}</label>
<input
id="name"
{...register('name')}
className="border border-neutral-300 p-2 text-sm"
/>
<p>{errors.name && t(camelCase(`name_${errors.name.type}`))}</p>
</div>

<div className="flex flex-col max-w-2xl w-full">
<label className="italic">{t('labelSlug')}</label>
<input
id="slug"
{...register('slug')}
className="border border-neutral-300 p-2 text-sm"
/>
<p>{errors.slug && t(camelCase(`slug_${errors.slug.type}`))}</p>
</div>

<div className="flex flex-col max-w-2xl w-full">
<label className="italic">{t('labelDescription')}</label>
<textarea
id="description"
{...register('description')}
rows={4}
className="border border-neutral-300 p-2 text-sm h-56"
/>
<p>
{errors.description &&
t(camelCase(`description_${errors.description.type}`))}
</p>
</div>

<div className="flex flex-col max-w-2xl w-full">
<label className="italic">{t('labelAttributionId')}</label>
<input
id="attributionId"
{...register('attributionId')}
className="border border-neutral-300 p-2 text-sm"
/>
<p>
{errors.attributionId &&
t(camelCase(`attributionId_${errors.attributionId.type}`))}
</p>
</div>

<div className="flex flex-row max-w-2xl w-full gap-2">
<button
disabled={isSubmitting}
type="submit"
className="p-1 sm:py-2 sm:px-3 rounded-full text-xs bg-neutral-200 hover:bg-neutral-300
text-neutral-800 transition flex items-center gap-1"
>
{t('editCommunitySaveButton')}
</button>
<button
className="p-1 sm:py-2 sm:px-3 rounded-full text-xs bg-neutral-200 hover:bg-neutral-300
text-neutral-800 transition flex items-center gap-1"
onClick={() => setIsVisible(slideOutId, false)}
>
{t('editCommunityCancelButton')}
</button>
</div>
</form>
);
}
Loading

2 comments on commit fbdad82

@vercel
Copy link

@vercel vercel bot commented on fbdad82 Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on fbdad82 Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.