From 28af0314b92393f9edee7781ec7b08f0da3511bb Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 26 Jun 2026 09:17:14 +0300 Subject: [PATCH 1/2] PM-5309 - projects showcase post management --- .../ProjectsShowcaseFilter.tsx | 6 +- .../form/FormSelectField/FormSelectField.tsx | 15 + .../lib/models/ProjectShowcasePost.model.ts | 3 + .../project-showcase-posts.service.ts | 173 ++++++ .../ProjectShowcasePage.module.scss | 45 +- .../ProjectShowcasePage.tsx | 557 +++++++++++++++++- 6 files changed, 781 insertions(+), 18 deletions(-) diff --git a/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx b/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx index f96532ccd..5c498bb81 100644 --- a/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx +++ b/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx @@ -5,13 +5,11 @@ import { Button, IconOutline, } from '~/libs/ui' +import { FormSelectOption } from '../form' import styles from './ProjectsShowcaseFilter.module.scss' -interface SelectOption { - label: string - value: string -} +type SelectOption = FormSelectOption interface ProjectsShowcaseFilterProps { keywordInput: string diff --git a/src/apps/work/src/lib/components/form/FormSelectField/FormSelectField.tsx b/src/apps/work/src/lib/components/form/FormSelectField/FormSelectField.tsx index 9d0af206b..a001ead1b 100644 --- a/src/apps/work/src/lib/components/form/FormSelectField/FormSelectField.tsx +++ b/src/apps/work/src/lib/components/form/FormSelectField/FormSelectField.tsx @@ -168,6 +168,20 @@ export const FormSelectField: FC = (props: FormSelectField ? props.fromFieldValue(field.value, options) : defaultFromFieldValue(field.value, options, isMulti) + const selectStyles = useMemo( + () => ({ + menu: (provided: Record) => ({ + ...provided, + zIndex: 9999, + }), + menuPortal: (provided: Record) => ({ + ...provided, + zIndex: 9999, + }), + }), + [], + ) + const handleSelectChange = useCallback( (selectedValue: SelectValue): void => { const normalizedValue = props.toFieldValue @@ -198,6 +212,7 @@ export const FormSelectField: FC = (props: FormSelectField isMulti={isMulti} loadOptions={props.loadOptions} menuPortalTarget={menuPortalTarget} + styles={selectStyles} onBlur={field.onBlur} onChange={handleSelectChange} options={options} diff --git a/src/apps/work/src/lib/models/ProjectShowcasePost.model.ts b/src/apps/work/src/lib/models/ProjectShowcasePost.model.ts index e39f571b4..b97365abf 100644 --- a/src/apps/work/src/lib/models/ProjectShowcasePost.model.ts +++ b/src/apps/work/src/lib/models/ProjectShowcasePost.model.ts @@ -10,7 +10,10 @@ export interface ProjectShowcasePostTaxonomyItem { export interface ProjectShowcasePost { id: string title: string + content?: string status: string + projectId?: string + challengeIds?: string[] createdAt: string createdById: number createdByHandle?: string diff --git a/src/apps/work/src/lib/services/project-showcase-posts.service.ts b/src/apps/work/src/lib/services/project-showcase-posts.service.ts index d3f93b87e..e5f1daa7e 100644 --- a/src/apps/work/src/lib/services/project-showcase-posts.service.ts +++ b/src/apps/work/src/lib/services/project-showcase-posts.service.ts @@ -1,6 +1,8 @@ import { xhrGetAsync, xhrGetPaginatedAsync, + xhrPatchAsync, + xhrPostAsync, } from '~/libs/core' import { @@ -13,6 +15,7 @@ import type { ProjectShowcasePost, ProjectShowcasePostCategory, ProjectShowcasePostIndustry, + ProjectShowcasePostTaxonomyItem, } from '../models' import { fetchMembersByUserIds } from './members.service' @@ -151,6 +154,136 @@ export async function fetchProjectShowcasePosts( } } +function normalizeTaxonomyArray(value: unknown): ProjectShowcasePostTaxonomyItem[] { + if (!Array.isArray(value)) { + return [] + } + + return value.map((item: any) => ({ + id: String(item?.id || ''), + name: String(item?.name || ''), + })) +} + +function normalizeString(value: unknown): string { + return value !== undefined && value !== null + ? String(value) + : '' +} + +function normalizeStringOrUndefined(value: unknown): string | undefined { + return value !== undefined && value !== null + ? String(value) + : undefined +} + +function normalizeProjectShowcasePost(value: unknown): ProjectShowcasePost | undefined { + if (typeof value !== 'object' || value === null) { + return undefined + } + + const post = value as Record + + return { + categories: normalizeTaxonomyArray(post.categories), + challengeIds: Array.isArray(post.challengeIds) + ? post.challengeIds.map((item: any) => String(item)) + : [], + content: normalizeStringOrUndefined(post.content), + createdAt: normalizeString(post.createdAt), + createdByHandle: normalizeStringOrUndefined(post.createdByHandle), + createdById: Number(post.createdById || 0), + id: normalizeString(post.id), + industries: normalizeTaxonomyArray(post.industries), + projectId: normalizeStringOrUndefined(post.projectId), + status: normalizeString(post.status), + title: normalizeString(post.title), + } +} + +export async function fetchProjectShowcasePost( + projectId: string, + postId: string, +): Promise { + try { + const response = await xhrGetAsync( + `${PROJECTS_API_URL}/${encodeURIComponent(projectId)}/posts/${encodeURIComponent(postId)}`, + ) + + const normalized = normalizeProjectShowcasePost(response) + if (!normalized) { + throw new Error('Failed to normalize showcase post') + } + + return normalized + } catch (error) { + throw normalizeError(error, 'Failed to fetch showcase post') + } +} + +export async function createProjectShowcasePost( + projectId: string, + payload: { + title: string + content: string + industryIds: string[] + categoryIds: string[] + challengeIds?: string[] + }, +): Promise { + try { + const response = await xhrPostAsync( + `${PROJECTS_API_URL}/${encodeURIComponent(projectId)}/posts`, + payload, + ) + + const normalized = normalizeProjectShowcasePost(response) + if (!normalized) { + throw new Error('Failed to normalize showcase post') + } + + return normalized + } catch (error) { + throw normalizeError(error, 'Failed to create showcase post') + } +} + +export async function updateProjectShowcasePost( + projectId: string, + postId: string, + payload: { + title?: string + content?: string + industryIds?: string[] + categoryIds?: string[] + challengeIds?: string[] + status?: string + }, +): Promise { + try { + const response = await xhrPatchAsync( + `${PROJECTS_API_URL}/${encodeURIComponent(projectId)}/posts/${encodeURIComponent(postId)}`, + payload, + ) + + const normalized = normalizeProjectShowcasePost(response) + if (!normalized) { + throw new Error('Failed to normalize showcase post') + } + + return normalized + } catch (error) { + throw normalizeError(error, 'Failed to update showcase post') + } +} + +export async function archiveProjectShowcasePost( + projectId: string, + postId: string, +): Promise { + return updateProjectShowcasePost(projectId, postId, { status: 'ARCHIVED' }) +} + function normalizeTaxonomyItem(value: unknown): ProjectShowcasePostCategory | undefined { if (typeof value !== 'object' || value === null) { return undefined @@ -172,6 +305,46 @@ function normalizeTaxonomyItem(value: unknown): ProjectShowcasePostCategory | un return { id, name: trimmedName } } +export async function createProjectShowcasePostIndustry( + name: string, +): Promise { + try { + const response = await xhrPostAsync<{ name: string }, unknown>( + `${PROJECTS_API_URL}/posts/industries`, + { name }, + ) + + const normalized = normalizeTaxonomyItem(response) + if (!normalized) { + throw new Error('Failed to normalize showcase post industry') + } + + return normalized + } catch (error) { + throw normalizeError(error, 'Failed to create showcase post industry') + } +} + +export async function createProjectShowcasePostCategory( + name: string, +): Promise { + try { + const response = await xhrPostAsync<{ name: string }, unknown>( + `${PROJECTS_API_URL}/posts/categories`, + { name }, + ) + + const normalized = normalizeTaxonomyItem(response) + if (!normalized) { + throw new Error('Failed to normalize showcase post category') + } + + return normalized + } catch (error) { + throw normalizeError(error, 'Failed to create showcase post category') + } +} + function sortTaxonomyItems(items: T[]): T[] { return items .slice() diff --git a/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.module.scss b/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.module.scss index ba695d676..303a47478 100644 --- a/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.module.scss +++ b/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.module.scss @@ -44,11 +44,6 @@ width: 100%; } -.actions { - display: flex; - justify-content: flex-start; -} - .errorBanner { max-width: 100%; } @@ -129,6 +124,46 @@ } } +.rowActions { + display: flex; + align-items: center; + gap: 10px; +} + +.actionButton { + color: $link-blue-dark; + font-size: 12px; + font-weight: 700; + background: transparent; + border: 0; + cursor: pointer; + padding: 0; + text-decoration: none; + + &:hover, + &:focus { + text-decoration: underline; + outline: none; + } +} + +.actionDelete { + color: #9f1f1f; +} + +.modalBody { + display: flex; + flex-direction: column; + gap: 16px; +} + +.modalFooter { + margin-top: 24px; + display: flex; + justify-content: flex-end; + gap: 8px; +} + .loadingRow, .emptyRow { diff --git a/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx b/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx index f22799af1..35ec8e6a6 100644 --- a/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx +++ b/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx @@ -10,9 +10,10 @@ import { import { useParams } from 'react-router-dom' import { SingleValue } from 'react-select' import classNames from 'classnames' +import { FormProvider, useForm } from 'react-hook-form' import { PageWrapper } from '~/apps/review/src/lib' -import { LoadingSpinner } from '~/libs/ui' +import { BaseModal, Button, LoadingSpinner, useConfirmationModal } from '~/libs/ui' import { ErrorMessage, @@ -20,11 +21,28 @@ import { ProjectListTabs, ProjectsShowcaseFilter, } from '../../../lib/components' +import { + fetchChallenges, + fetchChallenge, + archiveProjectShowcasePost, + createProjectShowcasePost, + createProjectShowcasePostCategory, + createProjectShowcasePostIndustry, + fetchProjectShowcasePost, + updateProjectShowcasePost, +} from '../../../lib/services' import { useFetchProjectShowcasePostCategories, useFetchProjectShowcasePostIndustries, useFetchProjectShowcasePosts, } from '../../../lib/hooks' +import { + FormMarkdownEditor, + FormSelectField, + FormSelectOption, + FormTextField, +} from '../../../lib/components/form' +import { showErrorToast, showSuccessToast } from '../../../lib/utils/toast.utils' import type { FetchProjectShowcasePostsParams, ProjectShowcasePost, @@ -35,10 +53,7 @@ import type { import styles from './ProjectShowcasePage.module.scss' -interface SelectOption { - label: string - value: string -} +type SelectOption = FormSelectOption const DEFAULT_FILTERS: ProjectShowcasePostFilters = { categoryId: undefined, @@ -119,6 +134,78 @@ function normalizeTaxonomyOption(item: ProjectShowcasePostCategory | ProjectShow } } +interface ProjectShowcasePostFormData { + title: string + content: string + industryIds: string[] + categoryIds: string[] + challengeId?: string +} + +function mapPostToFormData(post?: ProjectShowcasePost): ProjectShowcasePostFormData { + return { + title: post?.title || '', + content: post?.content || '', + industryIds: post?.industries.map(item => item.id) || [], + categoryIds: post?.categories.map(item => item.id) || [], + challengeId: post?.challengeIds?.[0] || undefined, + } +} + +async function loadProjectChallenges( + projectId: string, + inputValue: string, +): Promise { + const response = await fetchChallenges( + { projectId, name: inputValue }, + { page: 1, perPage: 20 }, + ) + + return response.data.map(challenge => ({ + label: challenge.name, + value: challenge.id, + })) +} + +async function resolveTaxonomyIds< + T extends ProjectShowcasePostCategory | ProjectShowcasePostIndustry, +>( + selectedIds: string[], + options: SelectOption[], + createEntity: (name: string) => Promise, +): Promise { + const existingIds = new Set(options.map(option => option.value).filter(Boolean)) + const createdNames: string[] = [] + const createdNameSet = new Set() + const resolvedIds: string[] = [] + + for (const selectedId of selectedIds) { + const trimmedId = selectedId.trim() + if (!trimmedId) { + continue + } + + if (existingIds.has(trimmedId)) { + resolvedIds.push(trimmedId) + continue + } + + if (!createdNameSet.has(trimmedId)) { + createdNameSet.add(trimmedId) + createdNames.push(trimmedId) + } + } + + const createdItems = await Promise.all( + createdNames.map(name => createEntity(name)), + ) + + return [ + ...resolvedIds, + ...createdItems.map(item => item.id), + ] +} + function createTaxonomyOptions( items: Array, defaultLabel: string, @@ -353,6 +440,171 @@ export const ProjectShowcasePage: FC = () => { const handleSortByIndustry = useCallback(() => handleSort('industry'), [handleSort]) const handleSortByCategory = useCallback(() => handleSort('category'), [handleSort]) + const [isManageModalOpen, setIsManageModalOpen] = useState(false) + const [manageMode, setManageMode] = useState<'create' | 'edit'>('create') + const [selectedPost, setSelectedPost] = useState(undefined) + const [selectedPostId, setSelectedPostId] = useState(undefined) + const [isLoadingPostDetails, setIsLoadingPostDetails] = useState(false) + const [selectedChallengeOption, setSelectedChallengeOption] = useState(undefined) + const [isSaving, setIsSaving] = useState(false) + const [formError, setFormError] = useState(undefined) + const [isPublishing, setIsPublishing] = useState(false) + const [isUnpublishing, setIsUnpublishing] = useState(false) + const [isRestoring, setIsRestoring] = useState(false) + const confirmation = useConfirmationModal() + + const handleOpenCreateModal = useCallback(() => { + setManageMode('create') + setIsManageModalOpen(true) + }, []) + + const handleEditPost = useCallback((postId: string) => { + setManageMode('edit') + setSelectedPostId(postId) + setIsManageModalOpen(true) + }, []) + + const handlePublishPost = useCallback(async (post: ProjectShowcasePost) => { + if (!projectId) { + return + } + + const confirmed = await confirmation.confirm({ + title: 'Publish Post', + content: ( +

+ Are you sure you want to publish the post{' '} + {post.title}? +

+ ), + }) + + if (!confirmed) { + return + } + + setIsPublishing(true) + + try { + await updateProjectShowcasePost(projectId, post.id, { + status: 'PUBLISHED', + }) + await postsResult.mutate() + showSuccessToast('Post published successfully') + } catch (error) { + showErrorToast(error instanceof Error ? error.message : 'Unable to publish post.') + } finally { + setIsPublishing(false) + } + }, [confirmation, postsResult, projectId]) + + const handleUnpublishPost = useCallback(async (post: ProjectShowcasePost) => { + if (!projectId) { + return + } + + const confirmed = await confirmation.confirm({ + title: 'Unpublish Post', + content: ( +

+ Are you sure you want to unpublish the post{' '} + {post.title}? +

+ ), + }) + + if (!confirmed) { + return + } + + setIsUnpublishing(true) + + try { + await updateProjectShowcasePost(projectId, post.id, { + status: 'DRAFT', + }) + await postsResult.mutate() + showSuccessToast('Post unpublished successfully') + } catch (error) { + showErrorToast(error instanceof Error ? error.message : 'Unable to unpublish post.') + } finally { + setIsUnpublishing(false) + } + }, [confirmation, postsResult, projectId]) + + const handleRestorePost = useCallback(async (post: ProjectShowcasePost) => { + if (!projectId) { + return + } + + const confirmed = await confirmation.confirm({ + title: 'Restore Post', + content: ( +

+ Are you sure you want to restore the archived post{' '} + {post.title}? +

+ ), + }) + + if (!confirmed) { + return + } + + setIsRestoring(true) + + try { + await updateProjectShowcasePost(projectId, post.id, { + status: 'DRAFT', + }) + await postsResult.mutate() + showSuccessToast('Post restored successfully') + } catch (error) { + showErrorToast(error instanceof Error ? error.message : 'Unable to restore post.') + } finally { + setIsRestoring(false) + } + }, [confirmation, postsResult, projectId]) + + const handleArchivePost = useCallback(async (post: ProjectShowcasePost) => { + if (!projectId) { + return + } + + const confirmed = await confirmation.confirm({ + title: 'Archive Post', + content: ( +

+ Are you sure you want to archive the post{' '} + {post.title}? +

+ ), + }) + + if (!confirmed) { + return + } + + try { + await archiveProjectShowcasePost(projectId, post.id) + await postsResult.mutate() + showSuccessToast('Post archived successfully') + } catch (error) { + showErrorToast(error instanceof Error ? error.message : 'Unable to archive post.') + } + }, [confirmation, postsResult, projectId]) + + const formMethods = useForm({ + defaultValues: mapPostToFormData(), + mode: 'all', + }) + + const { + handleSubmit, + reset, + setError, + } = formMethods + const handleResetFilters = useCallback(() => { setFilters({ categoryId: undefined, @@ -378,16 +630,86 @@ export const ProjectShowcasePage: FC = () => { .catch(() => undefined) }, [categoriesResult, industriesResult, postsResult]) + const formChallengeOptions = useMemo((): FormSelectOption[] => ( + selectedChallengeOption ? [selectedChallengeOption] : [] + ), [selectedChallengeOption]) + + const pageWrapperActions = useMemo(() => ( + + Actions {isLoading && filteredPosts.length === 0 && ( - + @@ -468,7 +791,7 @@ export const ProjectShowcasePage: FC = () => { {!isLoading && filteredPosts.length === 0 && ( - + No showcase posts found. @@ -497,6 +820,40 @@ export const ProjectShowcasePage: FC = () => { .map(item => item.name) .join(', ') || '—'} + + {post.status !== 'ARCHIVED' && ( + + )} + {post.status === 'DRAFT' && ( + + )} + {post.status === 'PUBLISHED' && ( + + )} + {post.status === 'ARCHIVED' ? ( + + ) : ( + + )} + ))} @@ -514,6 +871,188 @@ export const ProjectShowcasePage: FC = () => { /> )} + + { + setIsManageModalOpen(false) + setFormError(undefined) + }} + size='body' + > + +
+
+ {formError && ( +
+ +
+ )} + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ loadProjectChallenges(projectId ?? '', inputValue)} + options={formChallengeOptions} + placeholder='Select a challenge' + /> +
+
+ +
+
+
+
+
+ + {confirmation.modal} ) } From 3553b3bffd2878652ceec6ee8315ba4acc01f469 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 26 Jun 2026 10:32:06 +0300 Subject: [PATCH 2/2] lint --- .../ProjectsShowcaseFilter.tsx | 1 + .../ProjectShowcasePage.tsx | 193 ++++++++++-------- 2 files changed, 109 insertions(+), 85 deletions(-) diff --git a/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx b/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx index 5c498bb81..f83c588a6 100644 --- a/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx +++ b/src/apps/work/src/lib/components/ProjectsShowcaseFilter/ProjectsShowcaseFilter.tsx @@ -5,6 +5,7 @@ import { Button, IconOutline, } from '~/libs/ui' + import { FormSelectOption } from '../form' import styles from './ProjectsShowcaseFilter.module.scss' diff --git a/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx b/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx index 35ec8e6a6..3bb5e9a0a 100644 --- a/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx +++ b/src/apps/work/src/pages/showcase/ProjectShowcasePage/ProjectShowcasePage.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { ChangeEvent, FC, @@ -9,8 +10,8 @@ import { } from 'react' import { useParams } from 'react-router-dom' import { SingleValue } from 'react-select' +import { FormProvider, useForm, UseFormReturn } from 'react-hook-form' import classNames from 'classnames' -import { FormProvider, useForm } from 'react-hook-form' import { PageWrapper } from '~/apps/review/src/lib' import { BaseModal, Button, LoadingSpinner, useConfirmationModal } from '~/libs/ui' @@ -22,12 +23,12 @@ import { ProjectsShowcaseFilter, } from '../../../lib/components' import { - fetchChallenges, - fetchChallenge, archiveProjectShowcasePost, createProjectShowcasePost, createProjectShowcasePostCategory, createProjectShowcasePostIndustry, + fetchChallenge, + fetchChallenges, fetchProjectShowcasePost, updateProjectShowcasePost, } from '../../../lib/services' @@ -144,11 +145,11 @@ interface ProjectShowcasePostFormData { function mapPostToFormData(post?: ProjectShowcasePost): ProjectShowcasePostFormData { return { - title: post?.title || '', - content: post?.content || '', - industryIds: post?.industries.map(item => item.id) || [], categoryIds: post?.categories.map(item => item.id) || [], challengeId: post?.challengeIds?.[0] || undefined, + content: post?.content || '', + industryIds: post?.industries.map(item => item.id) || [], + title: post?.title || '', } } @@ -157,7 +158,7 @@ async function loadProjectChallenges( inputValue: string, ): Promise { const response = await fetchChallenges( - { projectId, name: inputValue }, + { name: inputValue, projectId }, { page: 1, perPage: 20 }, ) @@ -174,25 +175,21 @@ async function resolveTaxonomyIds< options: SelectOption[], createEntity: (name: string) => Promise, ): Promise { - const existingIds = new Set(options.map(option => option.value).filter(Boolean)) + const existingIds = new Set(options.map(option => option.value) + .filter(Boolean)) const createdNames: string[] = [] const createdNameSet = new Set() const resolvedIds: string[] = [] for (const selectedId of selectedIds) { const trimmedId = selectedId.trim() - if (!trimmedId) { - continue - } - - if (existingIds.has(trimmedId)) { - resolvedIds.push(trimmedId) - continue - } - - if (!createdNameSet.has(trimmedId)) { - createdNameSet.add(trimmedId) - createdNames.push(trimmedId) + if (trimmedId) { + if (existingIds.has(trimmedId)) { + resolvedIds.push(trimmedId) + } else if (!createdNameSet.has(trimmedId)) { + createdNameSet.add(trimmedId) + createdNames.push(trimmedId) + } } } @@ -442,7 +439,6 @@ export const ProjectShowcasePage: FC = () => { const [isManageModalOpen, setIsManageModalOpen] = useState(false) const [manageMode, setManageMode] = useState<'create' | 'edit'>('create') - const [selectedPost, setSelectedPost] = useState(undefined) const [selectedPostId, setSelectedPostId] = useState(undefined) const [isLoadingPostDetails, setIsLoadingPostDetails] = useState(false) const [selectedChallengeOption, setSelectedChallengeOption] = useState(undefined) @@ -470,13 +466,15 @@ export const ProjectShowcasePage: FC = () => { } const confirmed = await confirmation.confirm({ - title: 'Publish Post', content: (

- Are you sure you want to publish the post{' '} - {post.title}? + Are you sure you want to publish the post + {' '} + {post.title} + ?

), + title: 'Publish Post', }) if (!confirmed) { @@ -491,8 +489,8 @@ export const ProjectShowcasePage: FC = () => { }) await postsResult.mutate() showSuccessToast('Post published successfully') - } catch (error) { - showErrorToast(error instanceof Error ? error.message : 'Unable to publish post.') + } catch (err) { + showErrorToast(err instanceof Error ? err.message : 'Unable to publish post.') } finally { setIsPublishing(false) } @@ -504,13 +502,15 @@ export const ProjectShowcasePage: FC = () => { } const confirmed = await confirmation.confirm({ - title: 'Unpublish Post', content: (

- Are you sure you want to unpublish the post{' '} - {post.title}? + Are you sure you want to unpublish the post + {' '} + {post.title} + ?

), + title: 'Unpublish Post', }) if (!confirmed) { @@ -525,8 +525,8 @@ export const ProjectShowcasePage: FC = () => { }) await postsResult.mutate() showSuccessToast('Post unpublished successfully') - } catch (error) { - showErrorToast(error instanceof Error ? error.message : 'Unable to unpublish post.') + } catch (err) { + showErrorToast(err instanceof Error ? err.message : 'Unable to unpublish post.') } finally { setIsUnpublishing(false) } @@ -538,13 +538,15 @@ export const ProjectShowcasePage: FC = () => { } const confirmed = await confirmation.confirm({ - title: 'Restore Post', content: (

- Are you sure you want to restore the archived post{' '} - {post.title}? + Are you sure you want to restore the archived post + {' '} + {post.title} + ?

), + title: 'Restore Post', }) if (!confirmed) { @@ -559,8 +561,8 @@ export const ProjectShowcasePage: FC = () => { }) await postsResult.mutate() showSuccessToast('Post restored successfully') - } catch (error) { - showErrorToast(error instanceof Error ? error.message : 'Unable to restore post.') + } catch (err) { + showErrorToast(err instanceof Error ? err.message : 'Unable to restore post.') } finally { setIsRestoring(false) } @@ -572,13 +574,15 @@ export const ProjectShowcasePage: FC = () => { } const confirmed = await confirmation.confirm({ - title: 'Archive Post', content: (

- Are you sure you want to archive the post{' '} - {post.title}? + Are you sure you want to archive the post + {' '} + {post.title} + ?

), + title: 'Archive Post', }) if (!confirmed) { @@ -589,8 +593,8 @@ export const ProjectShowcasePage: FC = () => { await archiveProjectShowcasePost(projectId, post.id) await postsResult.mutate() showSuccessToast('Post archived successfully') - } catch (error) { - showErrorToast(error instanceof Error ? error.message : 'Unable to archive post.') + } catch (err) { + showErrorToast(err instanceof Error ? err.message : 'Unable to archive post.') } }, [confirmation, postsResult, projectId]) @@ -603,7 +607,7 @@ export const ProjectShowcasePage: FC = () => { handleSubmit, reset, setError, - } = formMethods + }: UseFormReturn = formMethods const handleResetFilters = useCallback(() => { setFilters({ @@ -652,7 +656,6 @@ export const ProjectShowcasePage: FC = () => { reset(mapPostToFormData()) setSelectedChallengeOption(undefined) setFormError(undefined) - setSelectedPost(undefined) setSelectedPostId(undefined) return } @@ -664,7 +667,7 @@ export const ProjectShowcasePage: FC = () => { setIsLoadingPostDetails(true) fetchProjectShowcasePost(projectId, selectedPostId) .then(post => { - setSelectedPost(post) + setSelectedPostId(post.id) reset(mapPostToFormData(post)) setFormError(undefined) @@ -684,8 +687,8 @@ export const ProjectShowcasePage: FC = () => { setSelectedChallengeOption(undefined) } }) - .catch(error => { - setFormError(error instanceof Error ? error.message : 'Unable to load post details.') + .catch(err => { + setFormError(err instanceof Error ? err.message : 'Unable to load post details.') }) .finally(() => { setIsLoadingPostDetails(false) @@ -708,7 +711,7 @@ export const ProjectShowcasePage: FC = () => {
@@ -823,35 +826,50 @@ export const ProjectShowcasePage: FC = () => { {post.status !== 'ARCHIVED' && ( + onClick={function onClick() { handleEditPost(post.id) }} + > + Edit + )} {post.status === 'DRAFT' && ( + onClick={function onClick() { handlePublishPost(post) }} + > + Publish + )} {post.status === 'PUBLISHED' && ( + onClick={function onClick() { handleUnpublishPost(post) }} + > + Unpublish + )} {post.status === 'ARCHIVED' ? ( + onClick={function onClick() { handleRestorePost(post) }} + > + Restore + ) : ( + onClick={function onClick() { handleArchivePost(post) }} + > + Archive + )} @@ -875,7 +893,7 @@ export const ProjectShowcasePage: FC = () => { { + onClose={function onClose() { setIsManageModalOpen(false) setFormError(undefined) }} @@ -884,7 +902,7 @@ export const ProjectShowcasePage: FC = () => {
{ if (!projectId) { return } @@ -893,16 +911,19 @@ export const ProjectShowcasePage: FC = () => { setIsSaving(true) if (!data.title.trim()) { - setError('title', { type: 'required', message: 'Title is required.' }) + setError('title', { message: 'Title is required.', type: 'required' }) } + if (!data.content.trim()) { - setError('content', { type: 'required', message: 'Content is required.' }) + setError('content', { message: 'Content is required.', type: 'required' }) } + if (!data.industryIds.length) { - setError('industryIds', { type: 'required', message: 'Select at least one industry.' }) + setError('industryIds', { message: 'Select at least one industry.', type: 'required' }) } + if (!data.categoryIds.length) { - setError('categoryIds', { type: 'required', message: 'Select at least one category.' }) + setError('categoryIds', { message: 'Select at least one category.', type: 'required' }) } if ( @@ -917,23 +938,23 @@ export const ProjectShowcasePage: FC = () => { try { const resolvedIndustryIds = await resolveTaxonomyIds( - data.industryIds, - industryOptions, - createProjectShowcasePostIndustry, - ) - const resolvedCategoryIds = await resolveTaxonomyIds( - data.categoryIds, - categoryOptions, - createProjectShowcasePostCategory, - ) - - if (manageMode === 'create') { + data.industryIds, + industryOptions, + createProjectShowcasePostIndustry, + ) + const resolvedCategoryIds = await resolveTaxonomyIds( + data.categoryIds, + categoryOptions, + createProjectShowcasePostCategory, + ) + + if (manageMode === 'create') { await createProjectShowcasePost(projectId, { - title: data.title.trim(), - content: data.content.trim(), - industryIds: resolvedIndustryIds, categoryIds: resolvedCategoryIds, challengeIds: data.challengeId ? [data.challengeId] : [], + content: data.content.trim(), + industryIds: resolvedIndustryIds, + title: data.title.trim(), }) setIsManageModalOpen(false) await Promise.all([ @@ -944,11 +965,11 @@ export const ProjectShowcasePage: FC = () => { showSuccessToast('Post created successfully') } else if (selectedPostId) { await updateProjectShowcasePost(projectId, selectedPostId, { - title: data.title.trim(), - content: data.content.trim(), - industryIds: resolvedIndustryIds, categoryIds: resolvedCategoryIds, challengeIds: data.challengeId ? [data.challengeId] : [], + content: data.content.trim(), + industryIds: resolvedIndustryIds, + title: data.title.trim(), }) setIsManageModalOpen(false) await Promise.all([ @@ -958,9 +979,9 @@ export const ProjectShowcasePage: FC = () => { ]) showSuccessToast('Post updated successfully') } - } catch (error) { - const message = error instanceof Error - ? error.message + } catch (err) { + const message = err instanceof Error + ? err.message : 'Unable to save post.' setFormError(message) } finally { @@ -1022,7 +1043,9 @@ export const ProjectShowcasePage: FC = () => { name='challengeId' isAsync isClearable - loadOptions={inputValue => loadProjectChallenges(projectId ?? '', inputValue)} + loadOptions={function loadOptions(inputValue: string) { + return loadProjectChallenges(projectId ?? '', inputValue) + }} options={formChallengeOptions} placeholder='Select a challenge' /> @@ -1032,7 +1055,7 @@ export const ProjectShowcasePage: FC = () => {