aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/project-overview/project-overview.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-10 12:16:59 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:27 +0100
commitd7bcd93efcd4f1ae20678d0efa6777cfadc09a4e (patch)
tree714edfa84a8f3c53262c407ac9a2a79c9d2479b8 /src/components/organisms/project-overview/project-overview.tsx
parentf699802b837d7d9fcf150ff2bf00cd3c5475c87a (diff)
refactor(components): replace Overview with ProjectOverview component
* `cover` prop is now expecting a ReactElement (NextImage) * `meta` prop is now limited to a specific set of meta items * add a `name` prop to add an accessible name to the figure element
Diffstat (limited to 'src/components/organisms/project-overview/project-overview.tsx')
-rw-r--r--src/components/organisms/project-overview/project-overview.tsx193
1 files changed, 193 insertions, 0 deletions
diff --git a/src/components/organisms/project-overview/project-overview.tsx b/src/components/organisms/project-overview/project-overview.tsx
new file mode 100644
index 0000000..2b8be0e
--- /dev/null
+++ b/src/components/organisms/project-overview/project-overview.tsx
@@ -0,0 +1,193 @@
+import {
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ forwardRef,
+ useCallback,
+ type ReactElement,
+} from 'react';
+import { useIntl } from 'react-intl';
+import type { Maybe, ValueOf } from '../../../types';
+import {
+ Time,
+ type SocialWebsite,
+ Link,
+ SocialLink,
+ Figure,
+} from '../../atoms';
+import { MetaList, type MetaItemData } from '../../molecules';
+import styles from './project-overview.module.scss';
+
+export type Repository = {
+ id: Extract<SocialWebsite, 'Github' | 'Gitlab'>;
+ label: string;
+ url: string;
+};
+
+export type ProjectPopularity = {
+ count: number;
+ url?: string;
+};
+
+export type ProjectMeta = {
+ creationDate: string;
+ lastUpdateDate: string;
+ license: string;
+ popularity: ProjectPopularity;
+ repositories: Repository[];
+ technologies: string[];
+};
+
+const validMeta = [
+ 'creationDate',
+ 'lastUpdateDate',
+ 'license',
+ 'popularity',
+ 'repositories',
+ 'technologies',
+] satisfies (keyof ProjectMeta)[];
+
+const isValidMetaKey = (key: string): key is keyof ProjectMeta =>
+ (validMeta as string[]).includes(key);
+
+export type ProjectOverviewProps = Omit<
+ HTMLAttributes<HTMLDivElement>,
+ 'children'
+> & {
+ /**
+ * The project cover.
+ */
+ cover?: ReactElement;
+ /**
+ * The project meta.
+ */
+ meta: Partial<ProjectMeta>;
+ /**
+ * The project name.
+ */
+ name: string;
+};
+
+const ProjectOverviewWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ ProjectOverviewProps
+> = ({ className = '', cover, meta, name, ...props }, ref) => {
+ const wrapperClass = `${styles.wrapper} ${className}`;
+ const intl = useIntl();
+ const coverLabel = intl.formatMessage(
+ {
+ defaultMessage: 'Illustration of {projectName}',
+ description: 'ProjectOverview: cover accessible name',
+ id: '701ggm',
+ },
+ { projectName: name }
+ );
+ const metaLabels = {
+ creationDate: intl.formatMessage({
+ defaultMessage: 'Created on:',
+ description: 'ProjectOverview: creation date label',
+ id: 'c0Oecl',
+ }),
+ lastUpdateDate: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'ProjectOverview: update date label',
+ id: 'JbT+fA',
+ }),
+ license: intl.formatMessage({
+ defaultMessage: 'License:',
+ description: 'ProjectOverview: license label',
+ id: 'QtdnFV',
+ }),
+ popularity: intl.formatMessage({
+ defaultMessage: 'Popularity:',
+ description: 'ProjectOverview: popularity label',
+ id: 'cIAOyy',
+ }),
+ repositories: intl.formatMessage({
+ defaultMessage: 'Repositories:',
+ description: 'ProjectOverview: repositories label',
+ id: '3bKzk0',
+ }),
+ technologies: intl.formatMessage({
+ defaultMessage: 'Technologies:',
+ description: 'ProjectOverview: technologies label',
+ id: 'OWkqXt',
+ }),
+ } satisfies Record<keyof ProjectMeta, string>;
+
+ const getMetaValue = useCallback(
+ (key: keyof ProjectMeta, value: ValueOf<ProjectMeta>) => {
+ if (typeof value === 'string') {
+ return key === 'license' ? value : <Time date={value} />;
+ }
+
+ if (
+ (value instanceof Object || typeof value === 'object') &&
+ !Array.isArray(value)
+ ) {
+ const stars = intl.formatMessage(
+ {
+ defaultMessage:
+ '{starsCount, plural, =0 {No stars} one {# star} other {# stars}}',
+ description: 'ProjectOverview: stars count',
+ id: 'PBdVsm',
+ },
+ { starsCount: value.count }
+ );
+
+ return value.url ? (
+ <>
+ ⭐&nbsp;<Link href={value.url}>{stars}</Link>
+ </>
+ ) : (
+ `⭐\u00A0${stars}`
+ );
+ }
+
+ return value.map((v) => {
+ if (typeof v === 'string') return { id: v, value: v };
+
+ return {
+ id: v.id,
+ value: <SocialLink icon={v.id} label={v.label} url={v.url} />,
+ };
+ });
+ },
+ [intl]
+ );
+
+ const getMetaItems = useCallback((): MetaItemData[] => {
+ const keys = Object.keys(meta).filter(isValidMetaKey);
+
+ return keys
+ .map((key): Maybe<MetaItemData> => {
+ const value = meta[key];
+
+ return value
+ ? {
+ id: key,
+ label: metaLabels[key],
+ value: getMetaValue(key, value),
+ hasBorderedValues: key === 'technologies',
+ hasInlinedValues:
+ (key === 'technologies' || key === 'repositories') &&
+ Array.isArray(value) &&
+ value.length > 1,
+ }
+ : undefined;
+ })
+ .filter((item): item is MetaItemData => typeof item !== 'undefined');
+ }, [getMetaValue, meta, metaLabels]);
+
+ return (
+ <div {...props} className={wrapperClass} ref={ref}>
+ {cover ? (
+ <Figure aria-label={coverLabel} className={styles.cover} hasBorders>
+ {cover}
+ </Figure>
+ ) : null}
+ <MetaList className={styles.meta} isInline items={getMetaItems()} />
+ </div>
+ );
+};
+
+export const ProjectOverview = forwardRef(ProjectOverviewWithRef);