Skip to content

Commit

Permalink
Edit community description
Browse files Browse the repository at this point in the history
  • Loading branch information
barbarah committed Sep 27, 2023
1 parent 40dfacd commit c73a116
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 16 deletions.
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>
);
}
38 changes: 30 additions & 8 deletions apps/researcher/src/app/[locale]/communities/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ 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';
import {SlideOutButton, SlideOut, SlideOutClosed, Notifications} from 'ui';
import EditDescriptionForm from './edit-description-form';

interface Props {
params: {
Expand All @@ -20,6 +21,11 @@ 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 Down Expand Up @@ -87,14 +93,30 @@ export default async function CommunityPage({params}: Props) {
{community.name}
</span>
</h1>

<div className="w-full flex flex-col md:flex-row justify-center px-4">
<div className="mb-4 max-w-3xl text-left">
{/*Place the description here*/}
</div>
<div className="flex flex-col items-start md:justify-center md:items-center w-full mb-4">
<JoinCommunityButton communityId={community.id} />
</div>
<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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export default function CommunityCard({community, locale}: CommunityCardProps) {
),
})}
</h1>
<div className="text-center p-4">
{/* TODO add community description */}
<div className="text-center m-4 line-clamp-3">
{community.publicMetadata?.description}
</div>

<div className="flex border-stone-300 border-t text-sm text-stone-600">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function addList(
try {
await objectList.createListForCommunity(listItem);
const community = await getCommunityById(listItem.communityId);
revalidatePath(`/[locale]/communities/${community.slug}`);
revalidatePath(`/[locale]/communities/${community.slug}`, 'page');

return {statusCode: 200};
} catch (err) {
Expand Down
20 changes: 19 additions & 1 deletion apps/researcher/src/lib/community.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
export type Community = Pick<
FullCommunity,
'id' | 'name' | 'slug' | 'imageUrl' | 'createdAt'
>;
> & {
publicMetadata?: {
description?: string;
};
};

export type Membership = Pick<OrganizationMembership, 'id' | 'role'> & {
publicUserData?: Pick<
Expand Down Expand Up @@ -118,3 +122,17 @@ export async function joinCommunity({
role: 'basic_member',
});
}

export async function editDescription({
communityId,
description,
}: {
communityId: string;
description: string;
}) {
return clerkClient.organizations.updateOrganizationMetadata(communityId, {
publicMetadata: {
description,
},
});
}
9 changes: 8 additions & 1 deletion apps/researcher/src/messages/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@
"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.",
"addObjectListButton": "Add list"
"addObjectListButton": "Add list",
"editDescriptionButton": "Edit description",
"labelDescription":"Description",
"changeDescriptionSaveButton": "Save",
"changeDescriptionCancelButton": "Cancel",
"descriptionSuccessfullyEdited": "Description has been updated",
"descriptionServerError": "Something went wrong while updating the description. Please try again later.",
"description_maxLength": "Description has a maximum of 2000 characters"
},
"AddObjectListForm": {
"title": "Add list",
Expand Down
9 changes: 8 additions & 1 deletion apps/researcher/src/messages/nl/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@
"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.",
"addObjectListButton": "Lijst toevoegen"
"addObjectListButton": "Lijst toevoegen",
"editDescriptionButton": "Beschrijving aanpassen",
"labelDescription":"Beschrijving",
"changeDescriptionSaveButton": "Opslaan",
"changeDescriptionCancelButton": "Annuleren",
"descriptionSuccessfullyEdited": "Beschrijving is succesvol aangepast",
"descriptionServerError": "Er is iets misgegaan bij het aanpassen van de beschrijving. Probeer het later opnieuw.",
"description_maxLength": "Beschrijving mag maximaal 2000 tekens bevatten"
},
"AddObjectListForm": {
"title": "Lijst toevoegen",
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/slide-out.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,20 @@ export const useSlideOut = create<SlideOutState>((set, get) => ({

interface SlideOutButtonProps {
id: string;
hideIfOpen?: Boolean;
children: ReactNode;
}

export function SlideOutButton({
id,
hideIfOpen = false,
children,
...buttonProps
}: SlideOutButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) {
const {setIsVisible, isVisible} = useSlideOut();

if (hideIfOpen && isVisible(id)) return null;

return (
<button {...buttonProps} onClick={() => setIsVisible(id, !isVisible(id))}>
{children}
Expand All @@ -53,3 +58,9 @@ export function SlideOut({id, children}: SlideOutProps) {
const {isVisible} = useSlideOut();
return isVisible(id) ? <>{children}</> : null;
}

// Use this component to render content when the slide out is closed
export function SlideOutClosed({id, children}: SlideOutProps) {
const {isVisible} = useSlideOut();
return !isVisible(id) ? <>{children}</> : null;
}

0 comments on commit c73a116

Please sign in to comment.