+
+
+ Go to the repository on GitHub
+
+
+ {name}
+
+
{description}
+
+
+
+ {stars}
+
+
+ {projectPathname}
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default Page;
diff --git a/src/components/card/card-content.tsx b/src/components/card/card-content.tsx
index 9af68d0..c834a0c 100644
--- a/src/components/card/card-content.tsx
+++ b/src/components/card/card-content.tsx
@@ -4,10 +4,28 @@ import { Slot } from '@radix-ui/react-slot';
import merge from '~/styles/merge';
+/**
+ * The props for the `CardContent` component.
+ *
+ * This interface extends the `div` HTML element attributes.
+ */
export interface CardContentProps extends ComponentPropsWithoutRef<'div'> {
+ /**
+ * The `asChild` property is a boolean that determines whether the component
+ * will forward the props to the first slottable child.
+ *
+ * @default false
+ */
asChild?: boolean;
}
+/**
+ * The `CardContent` is a React component that composes the content of the card.
+ *
+ * This component is meant to be used with the `Card` component.
+ *
+ * @props {@link CardContentProps}
+ */
function CardContent({
children,
className,
diff --git a/src/components/card/card-datetime.tsx b/src/components/card/card-datetime.tsx
index 835bd3d..2502cc4 100644
--- a/src/components/card/card-datetime.tsx
+++ b/src/components/card/card-datetime.tsx
@@ -4,6 +4,13 @@ import merge from '~/styles/merge';
export type CardDatetimeProps = ComponentPropsWithoutRef<'div'>;
+/**
+ * The `CardDatetime` component composes the text for the card's datetime.
+ *
+ * This component is meant to be used with the `Card` component.
+ *
+ * @props {@link CardDatetime}
+ */
function CardDatetime({
children,
className,
diff --git a/src/components/card/card-title.tsx b/src/components/card/card-title.tsx
index 6548dc2..8cab594 100644
--- a/src/components/card/card-title.tsx
+++ b/src/components/card/card-title.tsx
@@ -2,8 +2,20 @@ import type { ComponentPropsWithoutRef, JSX } from 'react';
import merge from '~/styles/merge';
+/**
+ * The props for the `CardTitle` component.
+ *
+ * This component extends the `h2` HTML element attributes.
+ */
export type CardTitleProps = ComponentPropsWithoutRef<'h2'>;
+/**
+ * The `CardTitle` is a React component that composes the title of the card.
+ *
+ * This component is meant to be used with the `Card` component.
+ *
+ * @props {@link CardTitleProps}
+ */
function CardTitle({
children,
className,
diff --git a/src/components/card/card.tsx b/src/components/card/card.tsx
index fe0a2d2..f929756 100644
--- a/src/components/card/card.tsx
+++ b/src/components/card/card.tsx
@@ -1,12 +1,48 @@
+'use client';
+
import type { ComponentPropsWithoutRef, JSX } from 'react';
+import { Slot } from '@radix-ui/react-slot';
+
import merge from '~/styles/merge';
-export type CardProps = ComponentPropsWithoutRef<'div'>;
+/**
+ * The props for the `Card` component.
+ */
+export interface CardProps extends ComponentPropsWithoutRef<'div'> {
+ /**
+ * The `asChild` prop is a boolean flag that determines if the card will
+ * forward the properties to the first slottable element.
+ *
+ * @default false
+ */
+ asChild?: boolean;
+}
+
+/**
+ * The `Card` is a React component that composes the card layout, acting as a
+ * wrapper for the whole card.
+ *
+ * @example
+ * ```tsx
+ *
+ * Card Title
+ * Card Content
+ *
+ * ```
+ *
+ * @props {@link CardProps}
+ */
+function Card({
+ children,
+ className,
+ asChild = false,
+ ...props
+}: CardProps): JSX.Element {
+ const RootElement = asChild ? Slot : 'article';
-function Card({ children, className, ...props }: CardProps): JSX.Element {
return (
-
{children}
-
+
);
}
diff --git a/src/components/image/image.tsx b/src/components/image/image.tsx
index c03d35f..ae27575 100644
--- a/src/components/image/image.tsx
+++ b/src/components/image/image.tsx
@@ -6,8 +6,17 @@ import merge from '~/styles/merge';
type Next$ImageProps = ComponentPropsWithoutRef;
+/**
+ * The props for the `Image` component.
+ */
export interface ImageProps extends Next$ImageProps {}
+/**
+ * The `Image` is a React component that composes the image element, applying
+ * the necessary styles.
+ *
+ * @props {@link ImageProps}
+ */
function Image({
src,
alt,
diff --git a/src/components/link-icon/link-icon.tsx b/src/components/link-icon/link-icon.tsx
index 95be0a3..118eef3 100644
--- a/src/components/link-icon/link-icon.tsx
+++ b/src/components/link-icon/link-icon.tsx
@@ -9,11 +9,41 @@ import merge from '~/styles/merge';
type PickedIconProps = Pick;
type PickedLinkProps = Omit;
+/**
+ * The props for the `LinkIcon` component.
+ */
export interface LinkIconProps extends PickedLinkProps, PickedIconProps {
+ /**
+ * The children of the link icon component which should be an icon element. In
+ * fact, this property should only carry one child element which renders an
+ * element via the `Icon` component.
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ */
children: ReactElement;
+ /**
+ * The content of the tooltip element that wraps the link icon. If not
+ * provided, this property defaults to `null` and is not created in the DOM.
+ *
+ * See {@link TooltipProps.content | `TooltipProps.content`} for more
+ * information on the accepted kinds of values.
+ *
+ * @default null
+ */
tooltipContent?: TooltipProps['content'];
}
+/**
+ * The `LinkIcon` is a React component that composes the link element displaying
+ * an icon and optionally wrapped in a tooltip.
+ *
+ * @props {@link LinkProps}
+ */
function LinkIcon({
children,
className,
diff --git a/src/components/link/link.tsx b/src/components/link/link.tsx
index f64d59b..400bf3d 100644
--- a/src/components/link/link.tsx
+++ b/src/components/link/link.tsx
@@ -6,12 +6,38 @@ import merge from '~/styles/merge';
type Next$LinkProps = ComponentPropsWithoutRef;
+/**
+ * The `LinkVariant` is a string literal that determines the appearance of the
+ * link component.
+ */
export type LinkVariant = 'primary' | 'secondary' | 'opaque';
+/**
+ * The props for the `Link` component.
+ */
export interface LinkProps extends Next$LinkProps {
+ /**
+ * The `variant` property is a string literal which determines the appearance
+ * of the link.
+ *
+ * - `primary`: The primary variant is used for primary actions with a purple
+ * text/foreground colour.
+ * - `secondary`: The secondary variant is used for secondary actions with a
+ * black text/foreground colour.
+ * - `opaque`: The opaque variant is used for tertiary actions with a zinc
+ * text/foreground colour.
+ *
+ * @default 'primary'
+ */
variant?: LinkVariant;
}
+/**
+ * The `Link` is a React component that composes the link element, applying
+ * the necessary styles and defining variants.
+ *
+ * @props {@link LinkProps}
+ */
function Link({
children,
className,
diff --git a/src/components/logo/logo.tsx b/src/components/logo/logo.tsx
new file mode 100644
index 0000000..7c08728
--- /dev/null
+++ b/src/components/logo/logo.tsx
@@ -0,0 +1,110 @@
+import type { HTMLAttributes, JSX } from 'react';
+
+import merge from '~/styles/merge';
+import styled, { type VariantProps } from '~/styles/styled';
+
+/**
+ * @internal The `createLogoStyles()` is a function that creates the styles for
+ * the `Logo` component.
+ */
+const createLogoStyles = styled(
+ merge(
+ 'bg-gradient-to-br rounded-lg w-8 h-8 shadow-2xl relative border',
+ 'from-zinc-50 to-zinc-200 text-black border-zinc-100',
+ 'dark:from-black dark:to-gray-900 dark:text-white dark:border-gray-800',
+ ),
+ {
+ variants: {
+ size: {
+ sm: 'w-6 h-6 text-base',
+ md: 'w-8 h-8 text-xl',
+ lg: 'w-12 h-12 text-2xl',
+ },
+ backdrop: {
+ shadow: 'shadow-2xl',
+ none: '',
+ },
+ },
+ },
+);
+
+/**
+ * The `LogoProps` interface defines the props for the `Logo` component.
+ */
+export interface LogoProps
+ extends HTMLAttributes,
+ VariantProps {
+ /**
+ * @ignore
+ */
+ children?: never;
+}
+
+/**
+ * @internal The class names for HTML span element when the size is "sm".
+ */
+const CHAR_SIZE_SM_CLASS_NAME = 'data-[size="sm"]:font-normal' as const;
+/**
+ * @internal The class names for HTML span element when the size is "md".
+ */
+const CHAR_SIZE_MD_CLASS_NAME = 'data-[size="md"]:font-medium' as const;
+/**
+ * @internal The class names for HTML span element when the size is "lg".
+ */
+const CHAR_SIZE_LG_CLASS_NAME = 'data-[size="lg"]:font-semibold' as const;
+
+const charSizesClassName = [
+ CHAR_SIZE_SM_CLASS_NAME,
+ CHAR_SIZE_MD_CLASS_NAME,
+ CHAR_SIZE_LG_CLASS_NAME,
+].join(' ');
+
+/**
+ * The `Logo` is a React component which renders the logo of the website.
+ *
+ * @props {@link LogoProps}
+ */
+function Logo({
+ className,
+ role = 'img',
+ 'aria-label': ariaLabel = 'Two letters L - one rotated 90 degrees and other 270 degrees',
+ size = 'md',
+ backdrop = 'shadow',
+ ...props
+}: LogoProps): JSX.Element {
+ return (
+
+
+ L
+
+
+ L
+
+
+ );
+}
+
+export default Logo;
diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx
index 81b4c2e..068c4b0 100644
--- a/src/components/tooltip/tooltip.tsx
+++ b/src/components/tooltip/tooltip.tsx
@@ -13,38 +13,116 @@ import { type ElementRef, type JSX, forwardRef } from 'react';
import merge from '~/styles/merge';
+/**
+ * @internal
+ */
type PickedRadix$TooltipProviderProps = Pick<
Radix$TooltipProviderProps,
'delayDuration' | 'disableHoverableContent' | 'skipDelayDuration'
>;
+/**
+ * @internal
+ */
+type PickedRadix$TooltipContentProps = Omit<
+ Radix$TooltipContentProps,
+ 'content' | 'align' | 'side'
+>;
+
+/**
+ * @internal
+ */
+type Radix$TooltipContentAlignProp = NonNullable<
+ Radix$TooltipContentProps['align']
+>;
+
+/**
+ * @internal
+ */
+type Radix$TooltipContentSideProp = NonNullable<
+ Radix$TooltipContentProps['side']
+>;
+
+/**
+ * The `TooltipPosition` type is a string literal type that represents the
+ * position of the tooltip content relative to the trigger node.
+ */
+export type TooltipPosition =
+ `${Radix$TooltipContentSideProp}-${Radix$TooltipContentAlignProp}`;
+
+/**
+ * The `TooltipProps` interface defines the props for the `Tooltip` component.
+ */
export interface TooltipProps
- extends Omit,
+ extends PickedRadix$TooltipContentProps,
PickedRadix$TooltipProviderProps {
+ /**
+ * The `content` property is the JSX element or string (typography) which will
+ * be displayed inside the tooltip when the user hovers over the trigger node
+ * tree provided via the children of this component.
+ *
+ * This property may also be `null` if the tooltip should not be displayed.
+ */
content: JSX.Element | string | null;
+ /**
+ * The `isArrow` property determines whether the tooltip should have an arrow
+ * or not.
+ *
+ * @default false
+ */
isArrow?: boolean;
+ /**
+ * The `TooltipPosition` property determines the position of the tooltip
+ * relative to the trigger node.
+ *
+ * @see {@link TooltipPosition}
+ *
+ * @default 'top-center'
+ */
+ position?: TooltipPosition;
}
+/**
+ * The `TooltipForwardedReferenceType` is the type of the forwarded reference
+ * passed down to the native HTML element.
+ */
export type TooltipForwardedReferenceType = ElementRef<
typeof Radix$PrimitiveContent
>;
+/**
+ * The `Tooltip` is a React Client Component (RCC) that wraps the content with
+ * an floating tooltip that appears when the user hovers over the trigger.
+ *
+ * @props {@link TooltipProps}
+ */
const Tooltip = forwardRef(
(
{
children,
className,
- sideOffset = 4,
+ sideOffset = 2,
content,
delayDuration,
disableHoverableContent,
skipDelayDuration,
isArrow = false,
+ position = 'top-center',
...props
},
ref,
) => {
- const isTriggerAsChild = typeof children !== 'string';
+ const [side, align] = position.split('-') as [
+ Radix$TooltipContentSideProp,
+ Radix$TooltipContentAlignProp,
+ ];
+ const isTriggerAsChild =
+ // NOTE: This is a type guard to check if the children is not a string.
+ typeof children !== 'string';
+
+ if (content === null) {
+ return children;
+ }
return (
(
{content}
- {isArrow && }
+
+ {isArrow && (
+
+ )}
diff --git a/src/components/top-bar/top-bar-button.tsx b/src/components/top-bar/top-bar-button.tsx
index 9170f23..7635a75 100644
--- a/src/components/top-bar/top-bar-button.tsx
+++ b/src/components/top-bar/top-bar-button.tsx
@@ -31,7 +31,7 @@ function TopBarButton({
href={href}
target={target}
className={merge(
- 'relative block px-3 py-2 transition hover:text-purple-500 dark:hover:text-purple-400',
+ 'relative block px-3 py-2 transition hover:text-purple-900 hover:bg-purple-100',
)}
>
{children}
diff --git a/src/constants/current-year.ts b/src/constants/current-year.ts
new file mode 100644
index 0000000..13928d7
--- /dev/null
+++ b/src/constants/current-year.ts
@@ -0,0 +1,3 @@
+const CURRENT_YEAR = new Date().getFullYear();
+
+export default CURRENT_YEAR;
diff --git a/src/constants/github.ts b/src/constants/github.ts
index 87f7255..2dec8b2 100644
--- a/src/constants/github.ts
+++ b/src/constants/github.ts
@@ -1,7 +1,8 @@
const GitHub = {
- PROFILE_URL: 'https://github.com/mrlemoos',
- AVATAR_URL: 'https://avatars.githubusercontent.com/u/69330304?v=4',
- REPOSITORY_URL: 'https://github.com/mrlemoos/mrlemoos.dev',
-} as const
+ USERNAME: 'mrlemoos',
+ PROFILE_URL: 'https://github.com/mrlemoos',
+ AVATAR_URL: 'https://avatars.githubusercontent.com/u/69330304?v=4',
+ REPOSITORY_URL: 'https://github.com/mrlemoos/mrlemoos.dev',
+} as const;
-export default GitHub
\ No newline at end of file
+export default GitHub;
diff --git a/src/domains/blog-post/components/copy-link-button.tsx b/src/domains/blog-post/components/copy-link-button.tsx
index fe0821f..be3cc65 100644
--- a/src/domains/blog-post/components/copy-link-button.tsx
+++ b/src/domains/blog-post/components/copy-link-button.tsx
@@ -1,6 +1,6 @@
'use client';
-import { type ButtonHTMLAttributes, Fragment, type JSX } from 'react';
+import type { ButtonHTMLAttributes, JSX } from 'react';
import Button from '~/components/button/button';
import useClipboardEventHandler from '~/hooks/use-clipboard-event-handler';
@@ -35,25 +35,25 @@ function CopyLinkButton({
const handleCopyLinkToClipboard = useClipboardEventHandler();
return (
-
+
-
+
);
}
diff --git a/src/domains/blog-post/read-post.tsx b/src/domains/blog-post/read-post.tsx
index e370ab9..bcab2e5 100644
--- a/src/domains/blog-post/read-post.tsx
+++ b/src/domains/blog-post/read-post.tsx
@@ -14,6 +14,7 @@ import type BlogPostModel from '~/domains/blog-post/models/blog-post-model';
import env from '~/domains/environment/env';
import date from '~/util/date';
import getHttpProtocol from '~/util/get-http-protocol';
+
import CopyLinkButton from './components/copy-link-button';
import createBlogPostPath from './util/create-blog-post-path';
@@ -144,8 +145,11 @@ function ReadPost({
DOCUMENT_TO_REACT_COMPONENTS_OPTIONS,
)}
-
- Like this post? Share it with your friends! 😃
+
+
+ Like this post?
+ Share it with your friends! 😃
+ Copy URL
diff --git a/src/domains/environment/env.ts b/src/domains/environment/env.ts
index 14781d3..8e48ca5 100644
--- a/src/domains/environment/env.ts
+++ b/src/domains/environment/env.ts
@@ -74,6 +74,11 @@ export interface EnvironmentVariables {
* authenticate with the Contentful Content Delivery API.
*/
CONTENTFUL_ACCESS_TOKEN: string;
+ /**
+ * The `GITHUB_USER_PINNED_REPOSITORIES` is the username of the GitHub user
+ * whose pinned repositories will be fetched.
+ */
+ GITHUB_USER_PINNED_REPOSITORIES: string;
}
/**
diff --git a/src/domains/footer/footer.tsx b/src/domains/footer/footer.tsx
index 1e3b8ee..82872c6 100644
--- a/src/domains/footer/footer.tsx
+++ b/src/domains/footer/footer.tsx
@@ -2,13 +2,12 @@ import type { JSX } from 'react';
import { GitHubLogoIcon, LinkedInLogoIcon } from '@radix-ui/react-icons';
+import LinkIcon from '~/components/link-icon/link-icon';
+import WildcardFooter from '~/components/wildcard-footer/wildcard-footer';
import Author from '~/constants/author';
-import LinkedIn from '~/constants/linked-in';
+import CURRENT_YEAR from '~/constants/current-year';
import GitHub from '~/constants/github';
-import WildcardFooter from '~/components/wildcard-footer/wildcard-footer';
-import LinkIcon from '~/components/link-icon/link-icon';
-
-const CURRENT_YEAR = new Date().getFullYear();
+import LinkedIn from '~/constants/linked-in';
/**
* The `Footer` is a React component that composes the footer section of the
diff --git a/src/domains/github/_/graphql-github-repository.ts b/src/domains/github/_/graphql-github-repository.ts
new file mode 100644
index 0000000..e0e63cc
--- /dev/null
+++ b/src/domains/github/_/graphql-github-repository.ts
@@ -0,0 +1,112 @@
+import {
+ ApolloClient,
+ InMemoryCache,
+ createHttpLink,
+ gql,
+} from '@apollo/client';
+import { setContext } from '@apollo/client/link/context';
+
+import GitHubRepository from '../github-repository';
+import GitHubPinnedRepositoryModel from '../models/github-pinned-repository-model';
+
+/**
+ * @internal The `PartialGitHubUser` is a partial and selective representation
+ * of a GitHub user.
+ */
+interface PartialGitHubUser {
+ pinnedItems: {
+ totalCount: number;
+ edges: {
+ node: {
+ name: string;
+ description: string;
+ id: string;
+ url: string;
+ stargazers: {
+ totalCount: number;
+ };
+ };
+ }[];
+ };
+}
+
+/**
+ * The `GraphQLGitHubRepository` class is an implementation of the
+ * `GitHubRepository` interface that uses the Apollo Client to fetch pinned
+ * repositories from the GitHub GraphQL API.
+ */
+export default class GraphQLGitHubRepository extends GitHubRepository {
+ private readonly HTTP_LINK = createHttpLink({
+ uri: 'https://api.github.com/graphql',
+ });
+ private readonly AUTH_CONTEXT = setContext((_, { headers }) => {
+ return {
+ headers: {
+ ...headers,
+ authorization: `Bearer ${process.env.GITHUB_ACCESS_TOKEN}`,
+ },
+ };
+ });
+
+ private readonly CLIENT = new ApolloClient({
+ link: this.AUTH_CONTEXT.concat(this.HTTP_LINK),
+ cache: new InMemoryCache(),
+ });
+
+ /**
+ * @inheritdoc
+ */
+ async fetchPinnedRepositories(
+ username: string,
+ limit = 6,
+ ): Promise {
+ try {
+ const { data } = await this.CLIENT.query<{ user: PartialGitHubUser }>({
+ query: gql`
+ {
+ user(login: "${username}") {
+ pinnedItems(first: ${limit}, types: [REPOSITORY]) {
+ totalCount
+ edges {
+ node {
+ ... on Repository {
+ name
+ description
+ id
+ url
+ stargazers {
+ totalCount
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `,
+ });
+
+ if (!data.user) {
+ return [];
+ }
+
+ const pinnedItems = data.user.pinnedItems.edges.map(
+ ({ node: { id, name, description, stargazers, url } }) =>
+ new GitHubPinnedRepositoryModel(
+ id,
+ name,
+ description,
+ url,
+ stargazers.totalCount,
+ ),
+ );
+
+ return pinnedItems;
+ } catch (error) {
+ console.error(
+ `ERROR: An error occurred while fetching pinned repositories: ${error}`,
+ );
+ }
+ return [];
+ }
+}
diff --git a/src/domains/github/create-github-repository.ts b/src/domains/github/create-github-repository.ts
new file mode 100644
index 0000000..34721eb
--- /dev/null
+++ b/src/domains/github/create-github-repository.ts
@@ -0,0 +1,12 @@
+import GraphQLGitHubRepository from './_/graphql-github-repository';
+import type GitHubRepository from './github-repository';
+
+/**
+ * The `createGitHubRepository()` function creates and returns a new instance of
+ * the `GitHubRepository` base class.
+ */
+export default function createGitHubRepository(
+ GitHubRepositoryImpl: new () => GitHubRepository = GraphQLGitHubRepository,
+): GitHubRepository {
+ return new GitHubRepositoryImpl();
+}
diff --git a/src/domains/github/github-repository.ts b/src/domains/github/github-repository.ts
new file mode 100644
index 0000000..39db30c
--- /dev/null
+++ b/src/domains/github/github-repository.ts
@@ -0,0 +1,22 @@
+import type GitHubPinnedRepositoryModel from './models/github-pinned-repository-model';
+
+export default abstract class GitHubRepository {
+ /**
+ * The `fetchPinnedRepositories()` method fetches a list of pinned
+ * repositories from GitHub for a given username.
+ */
+ abstract fetchPinnedRepositories(
+ /**
+ * The user name of the GitHub user to fetch pinned repositories for.
+ */
+ username: string,
+ /**
+ * The maximum number of pinned repositories to fetch.
+ *
+ * If not provided, this argument defaults to `6`.
+ *
+ * @default 6
+ */
+ limit?: number,
+ ): Promise;
+}
diff --git a/src/domains/github/models/github-pinned-repository-model.ts b/src/domains/github/models/github-pinned-repository-model.ts
new file mode 100644
index 0000000..edde5a7
--- /dev/null
+++ b/src/domains/github/models/github-pinned-repository-model.ts
@@ -0,0 +1,28 @@
+/**
+ * The `GitHubPinnedRepository` class represents a pinned repository from
+ * GitHub.
+ */
+export default class GitHubPinnedRepositoryModel {
+ constructor(
+ /**
+ * The identifier of the repository.
+ */
+ public readonly id: string,
+ /**
+ * The name of the repository from GitHub.
+ */
+ public readonly name: string,
+ /**
+ * The description of the repository from GitHub.
+ */
+ public readonly description: string,
+ /**
+ * The URL of the repository hosted on GitHub.
+ */
+ public readonly remoteURL: string,
+ /**
+ * The number of stars the repository has.
+ */
+ public readonly stars: number,
+ ) {}
+}
diff --git a/src/domains/header/header.tsx b/src/domains/header/header.tsx
index e30ea98..1bf72a3 100644
--- a/src/domains/header/header.tsx
+++ b/src/domains/header/header.tsx
@@ -1,5 +1,8 @@
import type { JSX } from 'react';
+import Link from '~/components/link/link';
+import Logo from '~/components/logo/logo';
+import Tooltip from '~/components/tooltip/tooltip';
import TopBar from '~/components/top-bar/top-bar';
import TopBarButton from '~/components/top-bar/top-bar-button';
import TopBarNavigation from '~/components/top-bar/top-bar-navigation';
@@ -12,7 +15,21 @@ import TopCenterCorner from '~/components/top-center-corner/top-center-corner';
function Header(): JSX.Element {
return (
-
+
+
+
+
+
+
About
diff --git a/src/domains/my-experience/components/work-experience-tile.tsx b/src/domains/my-experience/components/work-experience-tile.tsx
index 46bb10c..9559031 100644
--- a/src/domains/my-experience/components/work-experience-tile.tsx
+++ b/src/domains/my-experience/components/work-experience-tile.tsx
@@ -2,8 +2,16 @@ import type { JSX } from 'react';
import Image from 'next/image';
+import CURRENT_YEAR from '~/constants/current-year';
+
+/**
+ * The `WorkExperienceYear` type is a string representation of a year.
+ */
export type WorkExperienceYear = `${number}`;
+/**
+ * The props for the `WorkExperienceTile` component.
+ */
export interface WorkExperienceTileProps {
imageURL: string;
companyName: string;
@@ -12,8 +20,12 @@ export interface WorkExperienceTileProps {
endYear: WorkExperienceYear | 'Present';
}
-const CURRENT_YEAR = `${new Date().getFullYear()}`;
-
+/**
+ * The `WorkExperienceTile` is a React component that displays a single work
+ * experience entry.
+ *
+ * @props {@link WorkExperienceTileProps}
+ */
function WorkExperienceTile({
imageURL,
companyName,
@@ -50,7 +62,9 @@ function WorkExperienceTile({
>
-
-