Skip to content
Merged
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
10 changes: 9 additions & 1 deletion docs/work-showcase-win.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ The Challenge, The Solution and Business Impact Realised use the same Markdown
editor and sanitized rich-text preview. Key Win and Owner are single-line strings
of up to 255 characters. Existing title, taxonomy and solution requirements remain.

The Work preview modal and the customer portal showcase (`/showcase` cards and
post pages) share `ShowcasePostDetails`. Type, Customer, SMU (the custom value
for Others), Deal Close Date, Key Win, Current Status and Owner appear in a
labelled summary, followed by The Challenge, The Solution and Business Impact
Realised. Anything left blank is hidden, including empty media, challenge,
stats and skills sections. Showcase cards show Type, Customer and Current Status
when they are set.

**Send to WIN** is unchecked for new posts and retains the saved choice on edits.
Saving with the checkbox selected makes the record available to authorized callers
of `GET /v6/reports/WIN`. This is a pull integration: the success message confirms
Expand All @@ -45,5 +53,5 @@ deploy the reports API change for WIN callers. No new UI environment variables a

Use `.nvmrc`, then `yarn lint`, `yarn run build`, and the tests for
`ProjectShowcasePage`, `ProjectEditorForm`, `project-editor.schema`, and
`showcase-post.schema`. Manually check that editing metadata in either form is
`showcase-post.schema` and `ShowcasePostDetails`. Manually check that editing metadata in either form is
reflected when reopening the other form, and that opting in/out changes the WIN report.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
gap: 12px;
}

.highlights {
color: $black-80;
font-size: 14px;
font-weight: 600;
line-height: 22px;
overflow-wrap: anywhere;
}

.taxonomy {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,52 @@ interface ProjectShowcaseCardProps {
post: ProjectShowcasePost
}

const ProjectShowcaseCard: FC<ProjectShowcaseCardProps> = props => (
<article className={styles.wrap}>
<div className={styles.tags}>
{props.post.categories.map(category => (
<span className={classNames(styles.tag, toClassName(category.name))} id={category.id}>
{category.name}
</span>
))}
</div>

<h3 className={styles.title} title={props.post.title || 'Untitled'}>
{props.post.title || 'Untitled'}
</h3>
<div className={styles.taxonomy}>
<IconOutline.OfficeBuildingIcon className={classNames('icon-lg', styles.industryIcon)} />
<span>
{props.post.industries.map(item => item.name)
.join(', ') || '—'}
</span>
</div>

<div className={styles.content}>
{renderRichTextToPlainText(props.post.content || '')}
</div>

<div className={styles.button}>
<LinkButton
size='sm'
label='View details'
secondary
to={getPostRoute(props.post.projectId as string, props.post.id)}
/>
</div>
</article>
)
const ProjectShowcaseCard: FC<ProjectShowcaseCardProps> = props => {
const industries = props.post.industries.map(item => item.name)
.join(', ')
const highlights = [props.post.type, props.post.customer, props.post.currentStatus]
.map(value => value?.trim())
.filter(Boolean)
.join(' · ')
const summary = props.post.content || props.post.challenge || props.post.businessImpact || ''

return (
<article className={styles.wrap}>
<div className={styles.tags}>
{props.post.categories.map(category => (
<span className={classNames(styles.tag, toClassName(category.name))} id={category.id}>
{category.name}
</span>
))}
</div>

<h3 className={styles.title} title={props.post.title || 'Untitled'}>
{props.post.title || 'Untitled'}
</h3>
{highlights && <div className={styles.highlights}>{highlights}</div>}
{industries && (
<div className={styles.taxonomy}>
<IconOutline.OfficeBuildingIcon className={classNames('icon-lg', styles.industryIcon)} />
<span>{industries}</span>
</div>
)}

{summary && (
<div className={styles.content}>
{renderRichTextToPlainText(summary)}
</div>
)}

<div className={styles.button}>
<LinkButton
size='sm'
label='View details'
secondary
to={getPostRoute(props.post.projectId as string, props.post.id)}
/>
</div>
</article>
)
}

export default ProjectShowcaseCard
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { IconOutline, LinkButton, PageTitle } from '~/libs/ui'
import { renderRichTextToHtml } from '~/libs/shared/lib/utils/rich-text'
import { textFormatDateLocaleShortString } from '~/libs/shared/lib/utils/text-format'
import {
getShowcaseStorySections,
ShowcasePostDetails,
useFetchProjectShowcasePost,
UseFetchProjectShowcasePostResult,
} from '~/apps/work/src/lib'
Expand All @@ -30,6 +32,12 @@ const ProjectShowcasePostPage: FC = () => {
encodeURIComponent(routeParams.projectId as string),
].join('/')

const storySections = useMemo(
() => (post ? getShowcaseStorySections(post) : []),
[post],
)
const challengeCount = post?.challengeMetadata?.length ?? 0

const skills = useMemo(
() => uniqBy(post?.challengeMetadata?.flatMap(entry => entry.skills), 'id') ?? [],
[post?.challengeMetadata],
Expand Down Expand Up @@ -75,10 +83,12 @@ const ProjectShowcasePostPage: FC = () => {
{post?.title}
</h3>
<div className={styles.subTitle}>
<div className={styles.subTitleItem}>
<IconOutline.OfficeBuildingIcon className='icon-lg' />
<span>{industries}</span>
</div>
{industries && (
<div className={styles.subTitleItem}>
<IconOutline.OfficeBuildingIcon className='icon-lg' />
<span>{industries}</span>
</div>
)}
<div className={styles.subTitleItem}>
<IconOutline.CalendarIcon className='icon-lg' />
<span>Published</span>
Expand All @@ -90,24 +100,34 @@ const ProjectShowcasePostPage: FC = () => {
</div>
<div className={styles.contentBodyWrap}>
<div className={styles.contentBody}>
<div
className={styles.htmlContent}
dangerouslySetInnerHTML={{
__html: renderRichTextToHtml(post?.content || ''),
}}
/>
<div className={styles.contentSection}>
<h5 className={styles.sectionTitle}>Media assets</h5>
<ShowcasePostMedia
assets={post?.media}
/>
</div>
<div className={styles.contentSection}>
<h5 className={styles.sectionTitle}>Challenges</h5>
<ShowcasePostChallengeList
challengeIds={post?.challengeIds}
/>
</div>
{post && <ShowcasePostDetails data={post} />}
{storySections.map(section => (
<div key={section.label} className={styles.contentSection}>
<h5 className={styles.sectionTitle}>{section.label}</h5>
<div
className={styles.htmlContent}
dangerouslySetInnerHTML={{
__html: renderRichTextToHtml(section.value),
}}
/>
</div>
))}
{!!post?.media?.length && (
<div className={styles.contentSection}>
<h5 className={styles.sectionTitle}>Media assets</h5>
<ShowcasePostMedia
assets={post.media}
/>
</div>
)}
{!!post?.challengeIds?.length && (
<div className={styles.contentSection}>
<h5 className={styles.sectionTitle}>Topcoder Challenge Launched</h5>
<ShowcasePostChallengeList
challengeIds={post.challengeIds}
/>
</div>
)}
</div>
<aside className={styles.contentBodySidebar}>
<div className={styles.panel}>
Expand Down Expand Up @@ -139,46 +159,50 @@ const ProjectShowcasePostPage: FC = () => {
{projectUrl}
</span>
</div>
<div className={styles.panel}>
<h5 className={styles.panelTitle}>Stats</h5>
<ul className={styles.statsList}>
<li>
<strong>
{post?.challengeMetadata.length}
</strong>
<span>Challenges</span>
</li>
<li>
<strong>
{registrantsCount}
</strong>
<span>Registrants</span>
</li>
<li>
{challengeCount > 0 && (
<div className={styles.panel}>
<h5 className={styles.panelTitle}>Stats</h5>
<ul className={styles.statsList}>
<li>
<strong>
{challengeCount}
</strong>
<span>Challenges</span>
</li>
<li>
<strong>
{registrantsCount}
</strong>
<span>Registrants</span>
</li>
<li>
<strong>
{countriesCount}
</strong>
<span>Countries</span>
</li>
</ul>
</div>
)}
{skills.length > 0 && (
<div className={styles.panel}>
<h5 className={styles.panelTitle}>Skills</h5>
<p>
This showcase includes
{' '}
<strong>
{countriesCount}
{skills.length}
{' '}
skills.
</strong>
<span>Countries</span>
</li>
</ul>
</div>
<div className={styles.panel}>
<h5 className={styles.panelTitle}>Skills</h5>
<p>
This showcase includes
{' '}
<strong>
{skills.length}
{' '}
skills.
</strong>
</p>
<ul className={styles.skillsList}>
{skills.map(skill => (
<li key={skill.id}>{skill.name}</li>
))}
</ul>
</div>
</p>
<ul className={styles.skillsList}>
{skills.map(skill => (
<li key={skill.id}>{skill.name}</li>
))}
</ul>
</div>
)}
</aside>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@import '@libs/ui/styles/includes';

.details {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: $sp-4 $sp-6;
margin: 0;
padding: $sp-4;
border-radius: 8px;
border: 1px solid $black-20;
background: $black-5;
}

.item {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}

.label {
color: $black-80;
font-family: Roboto;
font-size: 12px;
font-weight: 700;
line-height: 16px;
letter-spacing: 1px;
text-transform: uppercase;
}

.value {
margin: 0;
color: $black-100;
font-family: Roboto;
font-size: 16px;
font-weight: 500;
line-height: 24px;
overflow-wrap: anywhere;
}
Loading
Loading